Pull to refresh

Comments 51

я думаю как-то так

templateT move_if_rr( T&& val )
{
return std::move(val);
}

templateconst T& move_if_rr( const T& val )
{
return val;
}

но я не уверен
хабр съел теги
templateT move_if_rr( T&& val )
{
return std::move(val);
}

templateconst T& move_if_rr( const T& val )
{
return val;
}
подскажите как выкладывать код
тег не работает
< >
Но кошернее — тегом <source>
в редакторе есть тег <code> но он не сработал
предпросмотр тоже не работает
template<typename T>
T move_if_rr( T&& val )
{
  return std::move(val);
}

template<typename T>
const T& move_if_rr( const T& val )
{
  return val;
}
Нет, не правильно

move_if_rr должна иметь как минимум 2 шаблонных аргумента (потому что тип «условия» и тип «аргумента» совсем разные):
template< class T >
void f( T &&rr )
{
   // это следует читать как
   // "сделать std::move для some_arg, если rr имеет move семантику,
   // иначе просто передать some_arg"
   std::move_if_rr< T >( some_arg );
}


Почему у вас только один конструктор с параметрами? Сделайте два, один для T&& rr, другой для const T& r, и не нужно будет магии с move_if_rr(), в одном конструкторе будет обычное копирование member-а, а в другом std::move(rr.Get()), который вызовет move-конструктор у U.

Я вообще не вижу вариантов, когда был бы вызван dummy_copy, плюс я не вижу вариантов как можно написать move_if_rr с динамическим определением того rvalue это ссылка или нет, ему всегда будет приходить U&, а по одному только типу T невозможно определить тип ссылки.
С другой стороны, я сказал полную чушь только что, предложив вам написать templated copy constructor, да, надо либо писать по copy-constructor-у для каждого возможного U, либо вообще отказаться от templated move constructor-а, потому что это тоже, вообще говоря, неправильно.
Бесспорно, в нешаблонном классе, можно написать 2 конструктора, вопрос: зачем, если можно один? В случае 2х конструкторов вам придется повторить весь остальной код конструктора, не связанный с полем типа member.
Хотите Вы этого или нет, но в C++ для Вашего случая придется объявлять два конструктора:
some_class( some_class const& );
some_class( some_class&& );


Вариант
template<typename T> some_class( T&& );

не определяет конструктор копирования.
Если быть более точным, то не не определяет, а не объявляет.
Если простыми словами, то template Container(T&& rr) это даже не move-конструктор, это просто обычный конструктор, который принимает что-то по rvalue-ссылке, так что вопрос поставлен некорректно.
тут дело в том что, если в конструкторе 1 параметр — все ок. у нас есть 2 перегрузки: const & и &&
а теперь представим что параметров стало 5, необходимое количество перегрузок станет 32, чтоб учесть все варианты вызова
для этого и была придумана шаблонная форма
то есть мы определяем одну шаблонную функцию и внутри нее при помощи forvard&move правильно обрабатываем каждый параметр
они еще обещали добавить шаблон который в компайлтайме подскажет как был передан параметр в конкретном вызове
Вы понимаете что шаблонных copy и move конструкторов не может существовать?
объясните. так как я сам только почитывал инфу на тему, сам пока не пользовался этой семантикой
Компилятор никогда не вызовет ваши templated copy/move конструкторы в ситуации, когда семантически должно произойти копирование, напротив, будет вызван implicitly-generated copy constructor.
Более того, сама фраза «templated copy constructor» не имеет никакого смысла, потому что как только там появляется шаблон, конструктор становится обычным, без специальной семантики, как и любой другой конвертирующий конструктор.
Плюс, если сделать «templated move constructor», то можно вообще очень сильно огрести segfault-ов.
Приведите, пожалуйста, пример кода где копилятор «не вызовет» template конструктор. Потому что, мой компилятор так не умеет.

Пример:
Container< A > a;
 
// вызывается Container<A>::Container<A><Container<A> >(Container<A> &&)
Container< A > b( std::move( a ) );
 
// вызывается Container<A>::Container<A><Container<A> &>(Container<A> & rr)
Container< A > c( a );


Это очень легко видеть под отладкой
Компилятор никогда не вызовет т.н. «templated copy ctor» при инициализации, можете сами проверить.

Объявив шаблонный конструктор с rvalue-ref, вы добились того, что у вас теперь никогда не будет вызван вообще никакой конструктор копирования, что уже семантическое безумие, вам не кажется? Да, благодаря reference collapsing ваш чудо-конструктор будет вызван так, как указано выше, но это не будет move-constructor-ом, вы же понимаете? Поэтому, именно семантически, это не move semantics, это магия и волшебство, а когда в коде магия и волшебство, то этот код, зачасутю, говно.

Если очень хочется извратиться, и сделать семантически некорректный код, то да, можно написать волшебную чудо-функцию move_if_rr:

template<class T, class V>
V move_if_rr(const V& value) {
return value;
}

template<class T, class V>
typename boost::disable_if<
boost::is_reference,
V&&
>::type
move_if_rr(V& value) {
return std::move(value);
}

И с её помощью написать плохой код, окей.
Вы развиваете холивар. Конструкторы к теме move_if_rr, совершенно не при чем. То что в примере был написан конструтор, это всего лишь случайность, замените в уме конструктор на обычную forward-функцию.

Касательно конструкторов. Да, шаблонный конструктор не заменит стандартный, однако неверно говорить, что он какой-либо из них никогда не будет вызван. Если быть точным, то стандартный конструктор копирования для типа Т будет вызван если явно привести аргумент к типу «const T &», в остальных случаях будет вызван шаблонный конструктор. В случае, если шаблонный конструктор ведет себя так же, как и стандартный ничего страшного в этом нет. Впрочем, согласен, так лучше не делать. Но, как, я уже говорил, это был всего лишь пример.

Что касается вашей функции, то есть только одно замечание: возвращаемое значение в первой перегрузке должно быть «const V&», а не «V»: семантически std::move и std::forward ничего не копируют, они просто делают преобразование типа ссылки, поэтому move_if_rr, как расширение std::forward, тоже должна заниматься только этим.
Ну, я про это и говорил: в контексте, когда должен быть неявно вызван конструктор копирования, он вызван не будет и нужно применять специальное волшебство, чтобы восстановить нормальную семантику, а это не дело.
А функцию я на коленке написал, там ещё шаблонный параметр пропущен, да.
Я вас понял, условие перефразировал.
Шаблонных move и copy конструкторов действительно не может существовать. Могут существовать шаблонные конструкторы преобразования или просто шаблонные конструкторы.

Пример.

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

Причина возникновения «другого» типа вектора, может быть банальна: он каким-то образом оптимизирован для конкретных задач.
в этом cpp-next.com блоге есть пост с огромным холиваром на эту тему, но я не помню какой именно
Вопрос поставлен корректно.
Читайте UPD2, я написал, зачем нужна такая функция.
Надеюсь, подобный код вы активно используете в production.
Так ведь тут нету ничего сложного :)
Поробуйте открыть boost или stl :)
Есть как бы разница между кодом, который пишут для компилятора, и кодом, который пишут для людей.

Какой-нибудь boost::mpl или внутренняя реализация stl'я — это из первой категории. Если также выглядит ваш обычный код — мне жаль и вас и ваших коллег.
если дальше продолжать ваши рассуждения то окажется что профессиональный рост фтопку.

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

Так как любое высказывание, метод, подход и т.п. можно довести до маразма, если «продолжать их дальше и дальше».
Пока получился вот такой вариант:
template<typename T> struct helper
{
	template<typename U> struct result
	{
		typedef U&& type;
	};

	template<typename U> static U&& get( U& u )
	{
		return static_cast<U&&>(u);
	}
};

template<typename T> struct helper<T&>
{
	template<typename U> struct result
	{
		typedef U const& type;
	};

	template<typename U> static U const& get( U const& u )
	{
		return u;
	}
};

template<typename T, typename U>
typename helper<T>::template result<U>::type move_if_rr( U& v )
{
	return helper<T>::get( v );
}
> успех засчитывается за функцию, которая работает правильно при условии, что Вы ее ни разу не отлаживали

Не учел… Несколько раз компилировал (проверить синтаксические ошибки)…
Можно сократить:
template<typename T, typename U> struct helper
{
    typedef U&& result;

    static U&& get( U& u )
    {
        return static_cast<U&&>( u );
    }
};

template<typename T, typename U> struct helper<T&, U>
{
    typedef U const& result;

    static U const& get( U const& u )
    {
        return u;
    }
};

template<typename T, typename U>
typename helper<T,U>::result move_if_rr( U& u )
{
    return helper<T,U>::get( u );
}
Будет работать неправильно в ряде случаев:

Container c0;

const Container& get_container() {
return c0;
}

Container c1(get_container());


Будет вызван implicit copy constructor.
Парсер съел шаблоны, но, думаю, и так понятно о чем речь.
> Будет работать неправильно в ряде случаев:
Это не относится к реализации move_if_rr.

Уже не один раз в этой теме писали, что конструкция
 template< class T >  Container( T&& );

не работает в качестве конструктора копирования.
Я перефразировал условие, конструкторы попали под раздачу совершенно случайно.

Что касается реализации. Она работает, но с оговорками: move работает правильно, а вот с константностью беда, например, у вас (в типах):

«const B & == move_if_rr< A & >( B & )»

в данном случае не должно появляться константы в результате
> а вот с константностью беда
Не учел.

Убираем const из специализации шаблона helper. Получаем:
template<typename T, typename U> struct helper
{
    typedef U&& result;
    static U&& get( U& u ) { return static_cast<U&&>( u ); }
};

template<typename T, typename U> struct helper<T&, U>
{
    typedef U& result;
    static U& get( U& u ) { return u;}
};

template<typename T, typename U> typename helper<T,U>::result move_if_rr( U& u )
{
    return helper<T,U>::get( u );
}


Комментируем или правим четвертый блок static_assert-ов в представленном тесте.
Тест проходит.

Что касается четвертого блока assert-ов (для /**/const A /**/).
Непонятно почему все возвращаемые типы имеют &, а не &&?
> Что касается четвертого блока assert-ов (для /**/const A /**/).
> Непонятно почему все возвращаемые типы имеют &, а не &&?

Это именно те грабли.

Возьмем функцию:
template< class T >
void Fwd( T &&rr )
{
    g( std::forward< T >( rr ) );
}
Вызовем ее так:
const Dummy o;
Fwd( std::move( o ) );
В данном случае T == «const Dummy», при этом g будет вызвана с «const Dummy &&», однако «const T &&» — это нифига не rvalue-ссылка на Dummy (мы не можем ничего «переместить» из константного объекта). Т.е. «move_if_rr{ const Dummy }( arg )» не должна перемещать arg. Этого можно добиться либо отказом от rvalue-ссылки (как у меня), либо добавлением модификатора const к типу arg перед оборачиванием его в rvalue ссылку. Семантически это одно и то же, если только у вас нету специализации «g» именно для «const Dummy &&», что само по себе несколько странно. Выбор в пользу отказа от rvalue ссылки сделан исходя из названия функции:

«move_if_rr{ FWD }( arg )» — выполнить move для arg, если FWD аргумент имеет move семантику, в противном случае ничего не делать. «Ничего не делать» и есть эквивалент отказу от rvalue-ссылки. Почему FWD в случае FWD = «const Dummy» не имеет move семантики мы разобрались выше.
С учетом представленного просто добавляем строчку
template<typename T, typename U> struct helper<T const, U> : helper<T&, U> {};


Весь код:
template<typename T, typename U> struct helper
{
    typedef U&& result;
    static U&& get( U& u ) { return static_cast<U&&>( u ); }
};

template<typename T, typename U> struct helper<T&, U>
{
    typedef U& result;
    static U& get( U& u ) { return u;}
};

template<typename T, typename U> struct helper<T const, U> : helper<T&, U> {};

template<typename T, typename U> typename helper<T,U>::result move_if_rr( U& u )
{
    return helper<T,U>::get( u );
}

Тест пройден.

Много написанного, но хотелось довести данный вариант до конца. Просто не всегда хочется иметь зависимости от дополнительных библиотек.
Можно, конечно, вынести static_cast в реализацию move_if_rr (как в представленном автором решении):
template<typename T, typename U> struct helper
    { typedef U&& result; };
template<typename T, typename U> struct helper<T&, U>
    { typedef U& result; };
template<typename T, typename U> struct helper<T const, U>
     : helper<T&, U> {};

template<typename T,typename U> typename helper<T,U>::result move_if_rr(U&u)
{
    return static_cast<helper<T,U>::result>( u );
}
Пройдет ещё немного времени, и плюсовики совсем перестанут решать реальные проблемы, а будут думать, как же правильно заюзать все фичи языка с этим безумным синтаксисом. Здесь что-то не так. :)
Всё просто: чем сложнее задачи — тем сложнее инструмент.
Задача «вертикально забить трубу в землю» сложнее, чем «забить гвоздь», но рабочие на стройке не пытаются собрать трубозабиватель на месте — они используют уже готовые, на которых работать не сильно сложнее, чем работать с молотком.
Инструментарий с задачами усложняется, но обычно не экспоненциально, как С++. :)
Теперь представьте что вам нужно забить миллиард труб =)
Если суть задачи состоит в том, чтобы у rvalue-объектов можно было «утащить» внутренние члены-данные через аксессоры, а с lvalue-объектами работать как обычно, то словами формулировку задачи можно описать так:
Реализовать функцию для вызова в виде move_if_rr<A>(b), которая вернет rvalue-ссылку на значение b, если тип A является rvalue-ссылкой, иначе вернет b как есть. Тогда так и запишем, слово за словом, используя STL:
#define RET_TYPE \
    typename std::conditional< \
      std::is_rvalue_reference<E>::value \
    , typename std::remove_reference<I>::type&& \
    , I&& \
    >::type

template <typename E, typename I>
RET_TYPE move_if_rr(I&& i)
{
    return static_cast<RET_TYPE>(i);
}

В такой формулировке утилитная функция может быть довольно полезна для перемещения внутреннего свойства из временного объекта, вместо его копирования.
А еще полезнее она была бы в более общем виде:
#define RET_TYPE \
    typename std::conditional< \
      C \
    , typename std::remove_reference<I>::type&& \
    , I&& \
    >::type

template <bool C, typename I>
RET_TYPE move_if(I&& i)
{
    return static_cast<RET_TYPE>(i);
}

move_if(val) — выполнить перемещение, если condition истинно, иначе — оставить тип как есть. В нашем случае использование:
move_if<std::is_rvalue_reference<A>::value>(b);


Но, судя по тестовому коду и static_assert'ам, задачу нужно было решить в другом виде.
Хотя, рантайм проверка на Вашем юнит-тесте проходит, если исправить строку
member = move_if_rr< T >( rr.Get() );
на
member = move_if_rr< T&& >( rr.Get() );
, что как раз укладывается в моем представлении. Ведь rr имеет тип T&&, а не T.
Сильно не ругайте, это мое видение постановки и решения задачи.
Даже если исправить static assert-ы (в расчете на то, что нужно указывать как параметр T&&, а не T&), статический тест не пройдет, провалятся следующие проверки:
typedef A        T1;
typedef A       &T2;
typedef const A &T3;
typedef const A  T4;
 
// move_if_rr< FWD >( Arg ) = Res
//
// Я буду писать так:
// ( FWD, Arg ) ? Res
 
// Следующие выражения не будут верны:
// 1. ( T2 &&, const B && ) == const B & (Ваш код даст const B &&)
// 2. ( T2 &&, B && )           == B &           (Ваш код даст B &&)
// 3. ( T3 &&, const B && ) == const B & (Ваш код даст const B &&)
// 4. ( T3 &&, B && )           == B &           (Ваш код даст B &&)
// 5. ( T4 &&, const B && ) == const B & (Ваш код даст const B &&)
// 6. ( T4 &&, const B & )   == const B & (Ваш код даст const B &&)
// 7. ( T4 &&, B && )           == B &           (Ваш код даст B &&)
// 8. ( T4 &&, B & )             == B &           (Ваш код даст B &&)
По сути у вас 2 проблемы, одна из которых спорная:

1. (спорная) Если FWD не является «полноценной rvalue ссылкой», вы не удаляете возможно присутствующую rvalue ссылку из Arg. Проблема спорная потому, что move_if_rr подразумевает «ничего не делать», если аргумент не является «полноценной rvalue ссылкой», а мы тут ссылку убираем. Однако, я считаю такое поведение правильным, т.к. оно подчеркивает, что move будет выполнен ТОЛЬКО если в качестве FWD выступает «полноценная rvalue ссылка», вне зависимости от того, передали ли Arg через std::move или нет. В пользу этого подхода выступает тот факт, что std::forward ведет себя точно также: его поведение не меняется от того, передать ему аргумент через std::move или нет.

2. Вы заметили, что в предыдущем пункте я оперирую понятием «полноценная rvalue ссылка», так вот, если есть класс A, то «A &&» — это «полноценная rvalue ссылка», а вот «const A &&» — нет. Почему? Потому что вы никак не можете что-то переместить из константного объекта, ведь его для этого надо как минимум изменить.

Почему код «прошел» рантайм тест?
Рантайм тест был направлен на проверку того, что функция не упадет с каким-нибудь page fault, семантика тестировалась статическим тестом. Обе эти ситуации можно отловить и в рантайме: первая произойдет, если в код «memver = move_if_rr{ T && }( rr.Get() )» заменить на «memver = move_if_rr{ T && }( std::move( rr.Get() ) )», а вторая — если разрешить константному Container возвращать не константный member (move конструктор для member не вызвался только потому, что в конструктор передали «const A &&», а не «А &&»).
Касательно, названия move_if_rr, мне кажется, Вы его интерпретировали как «move_if_rvalue_reference», однако это не так.

Ноги растут отсюда:

Если не использовать форвардинг, то для достижения того же эффекта, что и с ним, нужно объявить минимум 2 функции:

void f( const A &rc );
void f( A &&rr );

С форвардингом это выглядит так:
templatevoid f( T &&rr );

Так вот rr в названии отсылает к rr в «void f( A &&rr );»:
«Переместить, если бы в отсутствии форвардинга вызывалась функция которая с rr»

Я согласен, что название не очень удачное, но лучше идей у меня до сих пор не появилось.
Sign up to leave a comment.

Articles