Подтвердить что ты не робот

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

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

#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)) {}
};

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

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

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

Но в списке инициализации класса мы не можем создавать временные переменные.

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

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

Спасибо

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

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

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
    {}
};

Ответ 3

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

  • Если аргумент unique_ptr на первом месте, нам не повезло
  • Слишком легко для клиентов 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};