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

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

#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 вызовет неопределенное поведение (вероятно, segfault). Если первый аргумент построен первым, то все будет работать как задумано.

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

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

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

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

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

Спасибо


person Matthew Fioravante    schedule 17.07.2014    source источник
comment
Если вы можете изменить конструктор B, вам не нужно ничего этого делать. Просто используйте один аргумент, unique_ptr<A>, и сделайте копию a->x в списке инициализации конструктора.   -  person Praetorian    schedule 18.07.2014
comment
Ну, я не хотел менять интерфейс B таким образом, чтобы поддерживать это конкретное использование. Инициализация x с помощью a->x может оказаться неожиданной и, следовательно, не должна требовать особого случая от B. Это зависит от контекста, но для конструктора, принимающего только unique_ptr, может быть более естественным инициализировать x некоторой константой по умолчанию вместо a->x. Если мы изменим B на ссылку unique_ptr по rvalue, мы дадим вызывающим абонентам больше гибкости бесплатно и не изменим интерфейс. Я не вижу причин, по которым аргумент unique_ptr должен передаваться здесь по значению.   -  person Matthew Fioravante    schedule 18.07.2014
comment
Вы правы, здесь нет недостатка в передаче по ссылке rvalue. Другая возможность — сохранить существующий конструктор B и добавить перегрузку, которая принимает только unique_ptr<A>. В этом случае подразумевается, что B инициализирует x из a->x. Какой из них вы выберете, действительно зависит от предполагаемого использования вашего класса.   -  person Praetorian    schedule 18.07.2014
comment
См. пост Скотта Мейерса, вдохновленный этим вопросом: scottmeyers.blogspot.com.au/2014/07/   -  person Jon    schedule 05.08.2014


Ответы (3)


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

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

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

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

person Praetorian    schedule 17.07.2014
comment
Не знал этого правила. Это исходит из Си? То есть поддерживает ли инициализация структуры в C это качество? - person Ryan Haining; 18.07.2014
comment
@RyanHaining В случае структур POD инициализация с использованием фигурных скобок является совокупной инициализацией, которая всегда была частью C++ (унаследованной от C). В C++11 добавлена ​​инициализация списка (именно ее я здесь и использую), которая также включает агрегатную инициализацию. Я почти уверен, что агрегатная инициализация всегда имеет указанный порядок оценки слева направо, потому что struct или class члены всегда инициализируются в том порядке, в котором они определены. - person Praetorian; 18.07.2014
comment
Я рассмотрю назначенные инициализаторы в C99, было бы разумно, если бы они по-прежнему оценивались в том порядке, в котором они определены, но если в стандарте указан порядок их появления, я могу ошибаться. - person Ryan Haining; 18.07.2014
comment
@RyanHaining Хорошее замечание по поводу назначенных инициализаторов, я не знаю, что C99 говорит о них. В любом случае, все, что я сказал, относится только к C++. Возможно, это верно и для C, но я этого точно не знаю. - person Praetorian; 18.07.2014
comment
ПРИМЕЧАНИЕ: gcc долгое время был нарушение порядка оценки внутри списка braced-init-list; ошибка была исправлена ​​2014-07-01 (ствол). - person Filip Roséen - refp; 18.07.2014

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

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
    {}
};
person Jarod42    schedule 17.07.2014
comment
Почему первый ход не аннулирует a? Это потому, что std::move в основном просто приведение? - person Chris Drew; 18.07.2014
comment
@ChrisDrew Да, все, что вы там делаете, это привязываете его к ссылке. Фактическое перемещение внутренних элементов будет выполнено в списке инициализации частного конструктора C, когда будет создан аргумент для конструктора B. - person Praetorian; 18.07.2014

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

  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};
person Matthew Fioravante    schedule 17.07.2014
comment
@Deduplicator: никто не передает a.get() (необработанный указатель, который содержит unique_ptr) - person Ben Voigt; 18.07.2014
comment
@ Jarod42: Это собственный ответ спрашивающего. - person Peter O.; 18.07.2014
comment
Если нам разрешено изменять B, я бы предложил (прагматичное) решение добавить конструктор к B, который принимает только unique_ptr<A>, который устанавливает x внутри из a. - person Chris Drew; 18.07.2014