Универсальный ссылочный аргумент используется дважды

Я хочу создать оболочку вокруг std :: make_pair, которая принимает один аргумент и использует этот аргумент для создания первого и второго членов пары. Кроме того, я хочу воспользоваться семантикой перемещения.

Наивно, мы могли бы написать (игнорируя возвращаемые типы для ясности),

template <typename T>
void foo(T&& t)
{
  std::make_pair(std::forward<T>(t),
                 std::forward<T>(t));
}

но вряд ли это сделает то, что мы хотим.

Мы хотим:

  • В случае, когда foo вызывается с аргументом ссылки (const) lvalue, мы должны передать эту ссылку (const) в std :: make_pair без изменений для обоих аргументов.
  • В случае, когда foo вызывается с аргументом ссылки rvalue, мы должны продублировать указанный объект, а затем вызвать std :: make_pair с исходной ссылкой rvalue, а также ссылкой rvalue на вновь созданный объект.

На данный момент я придумал:

template <typename T>
T forward_or_duplicate(T t)
{
  return t;
}

template <typename T>
void foo(T&& t)
{
  std::make_pair(std::forward<T>(t),
                 forward_or_duplicate<T>(t));
}

Но я разумно уверен, что это неправильно.

Итак, вопросы:

  1. Это работает? Я подозреваю, что не в том, что если foo () вызывается со ссылкой на rvalue, тогда конструктор перемещения T (если он существует) будет вызываться при построении T, переданного по значению в forward_or_duplicate (), тем самым уничтожив t.

  2. Даже если это сработает, оптимально ли это? Опять же, я подозреваю, что не в том, что конструктор копирования T будет вызываться при возврате t из forward_or_duplicate ().

  3. Это похоже на обычную проблему. Есть идиоматическое решение?


person Christopher Key    schedule 11.08.2015    source источник
comment
Я думаю, вы могли бы сделать это просто std::make_pair(t, std::forward<T>(t)), если бы порядок между оценками параметров был определен (к сожалению, это не так).   -  person Cameron    schedule 12.08.2015
comment
@KerrekSB: Ха, не думал об этом. Вы должны отправить ответ :-)   -  person Cameron    schedule 12.08.2015
comment
На самом деле я не вижу ничего плохого в forward_or_duplicate.   -  person T.C.    schedule 12.08.2015
comment
@KerrekSB Я этого не вижу. make_pair принимает аргументы по ссылке, поэтому перемещение происходит внутри него, а копирование происходит до того, как вы введете make_pair.   -  person T.C.    schedule 12.08.2015
comment
@KerrekSB пересылает lvalue и дублирует rvalue.   -  person T.C.    schedule 12.08.2015
comment
@ T.C .: Тьфу, ничего, я пропустил, что T не выводится. Да, выглядит нормально.   -  person Kerrek SB    schedule 12.08.2015
comment
@Cameron: Как это часто бывает, я вызываю не make_pair (который был выбран, поскольку я думал, что это будет эквивалентным и иллюстративным), а вместо этого конструктор для контролируемого мной класса. Поскольку аргументы используются только для инициализации внутренних членов, я думаю, что порядок оценки хорошо определен.   -  person Christopher Key    schedule 12.08.2015
comment
@ChristopherKey Если вы не звоните make_pair, то то, что вы делаете, опасно. Порядок оценки не указан. Если класс идеально продвигается вперед, вы в безопасности, потому что привязка к ссылке ничего не делает, поэтому порядок не имеет значения: если вы берете что-либо по значению или берете что-либо по ссылке на другой тип, тогда вы будут возникать непредсказуемые и незаметные ошибки, которые трудно отследить, потому что порядок начинает иметь значение.   -  person Yakk - Adam Nevraumont    schedule 12.08.2015


Ответы (2)


Итак, вопросы:

  1. Это работает? Я подозреваю, что не в том, что если foo () вызывается со ссылкой на rvalue, тогда конструктор перемещения T (если он существует) будет вызываться при построении T, переданного по значению в forward_or_duplicate (), тем самым уничтожив t.

Нет, t в foo является lvalue, поэтому построение T, переданного по значению в forward_or_duplicate() из t, вызывает конструктор копирования.

  1. Даже если это сработает, оптимально ли это? Опять же, я подозреваю, что не в том, что конструктор копирования T будет вызываться при возврате t из forward_or_duplicate ().

Нет, t - это параметр функции, поэтому возврат неявно перемещается, а не копируется.

Тем не менее, эта версия будет более эффективной и безопасной:

template <typename T>
T forward_or_duplicate(std::remove_reference_t<T>& t)
{
  return t;
}

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

person T.C.    schedule 11.08.2015
comment
Спасибо. Это отвечает на мои первоначальные вопросы. В моем исходном решении я использовал T forward_or_duplicate (typename std :: enable_if ‹true, T› :: type t), чтобы поместить T в невыведенный контекст, но для ясности исключил его из вопроса. - person Christopher Key; 12.08.2015

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

Ваше решение не является оптимальным, потому что оно может скопировать T дважды, даже когда оно работает, когда ему нужно скопировать его только один раз.


Это, безусловно, самое простое решение. Он не исправляет тонкие сбои, вызванные изменением кода в другом месте, но если вы действительно вызываете make_pair, это не проблема:

template <typename T>
void foo(T&& t) {
  std::make_pair(std::forward<T>(t),
             static_cast<T>(t));
}

static_cast<T>(t) для выведенного типа T&& - это noop, если T&& - lvalue, и копия, если T&& - rvalue.

Конечно, static_cast<T&&>(t) также можно использовать вместо std::forward<T>(t), но люди этого тоже не делают.

Я часто так делаю:

template <typename T>
void foo(T&& t) {
  T t2 = t;
  std::make_pair(std::forward<T>(t),
             std::forward<T>(t2));
}

но это блокирует теоретическую возможность исключения (чего здесь нет).

В общем, вызов std::forward<T>(t) при вызове той же функции, что и static_cast<T>(t), или любой эквивалентной функции копирования или пересылки - плохая идея. Порядок, в котором оцениваются аргументы, не указан, поэтому, если аргумент, потребляющий std::forward<T>(t), не относится к типу T&&, а его конструктор видит rvalue T и перемещает состояние из него, static_cast<T>(t) может оценить после состояние t было вырвано.

Здесь этого не происходит:

template <typename T>
void foo(T&& t) {
  T t2 = t;
  std::make_pair(std::forward<T>(t),
             std::forward<T>(t2));
}

потому что мы перемещаем копирование или пересылку на другую строку, где инициализируем t2.

В то время как T t2=t; выглядит так, как будто он всегда копируется, если T&& является ссылкой lvalue, T также является ссылкой lvalue, а int& t2 = t; не копирует.

person Yakk - Adam Nevraumont    schedule 12.08.2015