Идеальная пересылка и конструкторы - PullRequest
0 голосов
/ 25 сентября 2018

Я пытаюсь понять взаимодействие идеального перенаправления и конструкторов.Мой пример следующий:

#include <utility>
#include <iostream>


template<typename A, typename B>
using disable_if_same_or_derived =
  std::enable_if_t<
    !std::is_base_of<
      A,
      std::remove_reference_t<B>
    >::value
  >;


template<class T>
class wrapper {
  public:
    // perfect forwarding ctor in order not to copy or move if unnecessary
    template<
      class T0,
      class = disable_if_same_or_derived<wrapper,T0> // do not use this instead of the copy ctor
    > explicit
    wrapper(T0&& x)
      : x(std::forward<T0>(x))
    {}

  private:
    T x;
};


class trace {
  public:
    trace() {}
    trace(const trace&) { std::cout << "copy ctor\n"; }
    trace& operator=(const trace&) { std::cout << "copy assign\n"; return *this; }
    trace(trace&&) { std::cout << "move ctor\n"; }
    trace& operator=(trace&&) { std::cout << "move assign\n"; return *this; }
};


int main() {
  trace t1;
  wrapper<trace> w_1 {t1}; // prints "copy ctor": OK

  trace t2;
  wrapper<trace> w_2 {std::move(t2)}; // prints "move ctor": OK

  wrapper<trace> w_3 {trace()}; // prints "move ctor": why?
}

Я хочу, чтобы у моего wrapper вообще не было служебных данных.В частности, при распределении временного объекта в оболочку, как и в случае w_3, я ожидал бы, что объект trace будет создан непосредственно на месте, без необходимости вызывать ctor перемещения.Тем не менее, есть вызов ctor move, который заставляет меня думать, что временный объект создается, а затем перемещается из.Почему называется ctor-ход?Как не назвать это?

Ответы [ 2 ]

0 голосов
/ 25 сентября 2018

Ссылка (либо ссылка lvalue, либо ссылка rvalue) должна быть привязана к объекту, поэтому, когда параметр ссылки x инициализируется, временный объект должен быть материализован в любом случае.В этом смысле совершенная пересылка не является «идеальной».

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

0 голосов
/ 25 сентября 2018

Я ожидаю, что объект трассировки будет создан непосредственно на месте, без необходимости вызывать ctor перемещения.

Я не знаю, почему вы этого ожидаете.Пересылка делает именно это: перемещает или копирует 1) .В вашем примере вы создаете временное значение с trace(), а затем переадресация перемещает его в x

. Если вы хотите создать объект T на месте, то вам нужно передать аргументы для построенияT, а не T объект, который нужно переместить или скопировать.

Создайте конструктор на месте:

template <class... Args>
wrapper(std::in_place_t, Args&&... args)
    :x{std::forward<Args>(args)...}
{}

И затем назовите его так:

wrapper<trace> w_3 {std::in_place};
// or if you need to construct an `trace` object with arguments;
wrapper<trace> w_3 {std::in_place, a1, a2, a3};

Направление комментария от ОП на другой ответ:

@ bolov Позволяет на минуту забыть о совершенной пересылке.Я думаю, что проблема заключается в том, что я хочу, чтобы объект был построен в конечном пункте назначения.Теперь, если его нет в конструкторе, теперь гарантируется, что это произойдет с гарантированным разрешением копирования / перемещения (здесь перемещение и копирование почти одинаковы).Чего я не понимаю, так это того, почему это не возможно в конструкторе.Мой тестовый пример доказывает, что этого не происходит в соответствии с текущим стандартом, но я не думаю, что это должно быть невозможно определить стандартом и реализовать компиляторами.Что мне не хватает, что такого особенного в ctor?

В ctor нет ничего особенного в этом отношении.Вы можете увидеть точно такое же поведение с помощью простой бесплатной функции:

template <class T>
auto simple_function(T&& a)
{
    X x = std::forward<T>(a);
    //  ^ guaranteed copy or move (depending on what kind of argument is provided
}

auto test()
{
    simple_function(X{});
}

Приведенный выше пример аналогичен вашему OP.Вы можете видеть simple_function как аналог вашего конструктора оболочки, а мою локальную переменную x как аналог вашего элемента данных в wrapper.Механизм в этом отношении тот же.

Чтобы понять, почему вы не можете создать объект непосредственно в локальной области действия simple_function (или как элемент данных в вашем объекте-оболочке в вашем случае), вынужно понять, как работает гарантированное копирование в C ++ 17 Я рекомендую этот отличный ответ .

Подводя итог этому ответу: в основном выражение prvalue не работаетматериализует объект, вместо этого он может инициализировать объект.Вы держите выражение как можно дольше, прежде чем использовать его для инициализации объекта (таким образом избегая некоторых копий / перемещений).Обратитесь к связанному ответу для более подробного, но дружественного объяснения.

В тот момент, когда ваше выражение используется для инициализации параметра simple_foo (или параметра вашего конструктора), вы вынуждены материализовать объекти потерять выражение лица.Отныне у вас больше нет оригинального выражения prvalue, у вас есть созданный материализованный объект.И этот объект теперь нужно переместить в конечный пункт назначения - мой локальный x (или ваш элемент данных x).

Если мы немного изменим мой пример, мы увидим гарантированное разрешение копирования при работе:

auto simple_function(X a)
{
    X x = a;
    X x2 = std::move(a);
}


auto test()
{
    simple_function(X{});
}

Без elision все будет выглядеть так:

  • X{} создает временный объект в качестве аргумента для simple_function.Давайте назовем это Temp1
  • Temp1 теперь перемещено (потому что это значение) в параметр a из simple_function
  • a копируется (потому что a является lvalue) в x
  • a перемещено (потому что std::move приводит a к xvalue) в x2

Теперь с C ++17 гарантированная копия elision

  • X{} больше не материализует объект на месте.Вместо этого выражение сохраняется.
  • параметр a из simple_function теперь можно инициализировать из выражения X{}.Никакое копирование или перемещение не требуется и не требуется.

Остальное теперь то же самое:

  • a скопировано в x1
  • a перемещено в x2

Что вам нужно понять: как только вы что-то назвали, что-то должно существовать.Удивительно простая причина этого в том, что если у вас есть имя для чего-то, вы можете ссылаться на него несколько раз.Смотрите мой ответ на этот другой вопрос .Вы назвали параметр wrapper::wrapper.Я назвал параметр simple_function.В этот момент вы теряете выражение prvalue для инициализации этого именованного объекта.


Если вы хотите использовать гарантированное копирование в C ++ 17, и вам не нравится метод на месте, который вам нуженчтобы не называть вещи :) Вы можете сделать это с помощью лямбды.Идиома, которую я вижу чаще всего, в том числе в стандарте, - это способ на месте.Так как я не видел лямбда-путь в дикой природе, я не знаю, рекомендую ли я это.В любом случае, это так:

template<class T> class wrapper {
public:

    template <class F>
    wrapper(F initializer)
        : x{initializer()}
    {}

private:
    T x;
};

auto test()
{
    wrapper<X> w = [] { return X{};};
}

В C ++ 17 это не дает никаких копий и / или перемещений, и это работает, даже если X удалил конструкторы копий и конструкторы перемещения.Объект будет построен в конечном пункте назначения, так же, как вы хотите.


1) Я говорю об идиоме переадресации при правильном использовании.std::forward это просто актерский состав.

...