Перенос семантики и оценки порядка функций
Предположим, что у меня есть следующее:
#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)
? Это поможет?
Спасибо
Ответы
Ответ 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};