Переместить семантику и оценку порядка функций

Предположим, у меня есть следующее:

#include <memory>
struct A { int x; };

class B {
  B(int x, std::unique_ptr<A> a);
};

class C : public B {
  C(std::unique_ptr<A> a) : B(a->x, std::move(a)) {}
};

Если я правильно понимаю правила C++ относительно "неопределенного порядка параметров функций", этот код небезопасен. Если второй аргумент Bконструктор сначала создается с помощью конструктора перемещения, затем a теперь содержит nullptr и выражение a->x вызовет неопределенное поведение (вероятно, segfault). Если первый аргумент создается первым, то все будет работать так, как задумано.

Если бы это был обычный вызов функции, мы могли бы просто создать временный:

auto x = a->x
B b{x, std::move(a)};

Но в списке инициализации класса у нас нет свободы для создания временных переменных.

Предположим, я не могу изменить BЕсть ли какой-нибудь возможный способ для достижения вышеизложенного? А именно разыменование и перемещение unique_ptr в том же выражении вызова функции без создания временного?

Что делать, если вы могли бы изменить Bконструктор, но не добавлять новые методы, такие как setX(int)? Это поможет?

Спасибо

4 ответа

Используйте инициализацию списка для создания B, Элементы гарантированно будут оцениваться слева направо.

C(std::unique_ptr<A> a) : B{a->x, std::move(a)} {}
//                         ^                  ^ - braces

Из §8.5.4/4 [dcl.init.list]

В пределах списка инициализатора списка фигурных скобок предложения инициализатора, включая любые, которые являются результатом расширений пакета (14.5.3), оцениваются в порядке, в котором они появляются. Таким образом, каждое вычисление значения и побочный эффект, связанный с данным предложением инициализатора, упорядочивается перед каждым вычислением значения и побочным эффектом, связанным с любым предложением инициализатора, которое следует за ним в разделенном запятыми списке списка инициализатора.

В качестве альтернативы ответу Преториана вы можете использовать делегат конструктора:

class C : public B {
public:
    C(std::unique_ptr<A> a) :
        C(a->x, std::move(a)) // this move doesn't nullify a.
    {}

private:
    C(int x, std::unique_ptr<A>&& a) :
        B(x, std::move(a)) // this one does, but we already have copied x
    {}
};

Предложение Преториана об использовании инициализации списка, кажется, работает, но у него есть несколько проблем:

  1. Если аргумент unique_ptr на первом месте, нам не повезло
  2. Это слишком просто для клиентов B случайно забыть использовать {} вместо (), Дизайнеры BИнтерфейс наложил эту потенциальную ошибку на нас.

Если бы мы могли изменить B, то, возможно, одно из лучших решений для конструкторов - всегда передавать unique_ptr по ссылке rvalue, а не по значению.

struct A { int x; };

class B {
  B(std::unique_ptr<A>&& a, int x) : _x(x), _a(std::move(a)) {}
};

Теперь мы можем безопасно использовать std::move().

B b(std::move(a), a->x);
B b{std::move(a), a->x};

Код не содержит неопределенного поведения. Это распространенное заблуждение, что std::move() на самом деле выполняет перемещение, но это не так. std::move() просто преобразует входные данные в ссылку на значение r, что является семантическим изменением времени компиляции и не имеет кода времени выполнения. Поэтому в заявлении:

      B(a->x, std::move(a))

Состояние 'a' не изменяется вызовом std::move(), поэтому неопределенное поведение отсутствует независимо от порядка вычислений.

Другие вопросы по тегам