А понимаете ли Вы move семантику?

    Добрый день!

    Еще вчера я честно думал, что разобрался как работают rvalue references в C++11. Думал до тех пор, пока не наткнулся на грабли.

    Итак, я предлагаю размять мозги и попробовать написать функцию move_if_rr. Такой себе тест на понимание: успех засчитывается за функцию, которая работает правильно при условии, что Вы ее ни разу не отлаживали. Детали задания под катом.

    Итак, условие.
    Пусть, есть следующий код:
    template< class U >
    class Container
    {   
        U member;
     
    public:
     
        U &Get(){ return member; }
        const U &Get() const{ return member; }
     
    public:
     
        template< class T > void dummy( const Container< T > & ); // copy
        template< class T > void dummy( Container< T > && ); // move
     
    public:
     
        // Ожидается, что T - это нечто совместимое с Container
        template< class T >
        void Fwd( T &&rr )
        {
            member = move_if_rr< T >( rr.Get() );
            // dummy( std::forward< T >( rr ) )
        }
    };
    Container намеренно сделан шаблонным, чтобы реальный тип member был неизвестен.
    Функции dummy нужны только, для того, чтобы разъяснить семантику move_if_rr (ниже по тексту).
    Приведенный выше код не несет никакой смысловой нагрузки, все совпадения считать случайными.

    Необходимо написать функцию move_if_rr, удовлетворяющую следующим правилам:
    Если закомментированный forward в Container.Fwd, вызовет dummy, помеченную как move:
    * Использование move_if_rr должно приводить к вызову move-assignment поля member.
    Если закомментированный forward в Containerю.Fwd, вызовет dummy, помеченную как copy:
    * Использование move_if_rr должно приводить к вызову copy-assignment поля member.
    Оба правила предполагают, что в качестве аргумента Container.Fwd был передан тип Container.

    Решения ожидаются в комментариях.

    UPD1. Чтобы выложить код можно использовать highlight.hohli.com (нужно поставить галочку «Use font tag (for Habrahabr)»)

    UPD2. Для тех кто не понял, что такое move_if_rr:
    Это расширенная версия std::forward: Используя std::forward нельзя указать в качестве шаблонного аргумента один тип, а в качестве параметра — другой, move_if_rr решает именно эту задачу. Зачем?
    потому что если мне передали некий тип по rvalue ссылке, то и все внутренние его компоненты тоже можно рассматривать как переданные по rvalue ссылке. Только вот как это объяснить компилятору, если я не указывал явно как мне передавали этот самый некий тип, а использовал стандартную forward-нотацию?
    Пример:
    Допустим была вот такая функция:
    template< class T >
    void f( T &&rr )
    {
        SendData( std::forward< T >( rr ) );
    }
    И тут что-то изменили в программе, и в нее стал приходить композитный тип (старый тип в нем есть как поле). Как передать это поле с форвардингом, как и раньше? C move_if_rr это выглядит так:
    template< class T >
    void f( T &&rr )
    {
         SendData( std::move_if_rr< T >( rr.GetSomeElement() ) );
    }

    UPD 3. Условие немного перефразировано, т.к. предыдущая формулировка позволяла цепляться к семантике конструкторов (вместо Fwd использовался конструктор).

    UPD 4. Проверить решение можно вставив его в начало этого кода, полностью корректный вариант можно найти тут, есть также вариант от SergX — без использования mpl

    Комментарии 51

      +18
      Я тут посижу посмотрю.
        0
        я думаю как-то так

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

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

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

          templateconst T& move_if_rr( const T& val )
          {
          return val;
          }
            0
            подскажите как выкладывать код
            тег не работает
              0
              &lt; &gt;
              Но кошернее — тегом <source>
                +1
                в редакторе есть тег <code> но он не сработал
                предпросмотр тоже не работает
            0
            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;
            }
              0
              Нет, не правильно

              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 );
              }


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

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


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

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


                          Это очень легко видеть под отладкой
                            +2
                            Компилятор никогда не вызовет т.н. «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);
                            }

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

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

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

                        Пример.

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

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

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

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

                                Так как любое высказывание, метод, подход и т.п. можно довести до маразма, если «продолжать их дальше и дальше».
                          0
                          Пока получился вот такой вариант:
                          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 );
                          }
                            0
                            > успех засчитывается за функцию, которая работает правильно при условии, что Вы ее ни разу не отлаживали

                            Не учел… Несколько раз компилировал (проверить синтаксические ошибки)…
                              0
                              Можно сократить:
                              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 );
                              }
                              
                                0
                                Будет работать неправильно в ряде случаев:

                                Container c0;

                                const Container& get_container() {
                                return c0;
                                }

                                Container c1(get_container());


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

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

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

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

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

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

                                        Убираем 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 /**/).
                                        Непонятно почему все возвращаемые типы имеют &, а не &&?
                                          0
                                          > Что касается четвертого блока 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 семантики мы разобрались выше.
                                            0
                                            С учетом представленного просто добавляем строчку
                                            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 );
                                            }
                                            

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

                                            Много написанного, но хотелось довести данный вариант до конца. Просто не всегда хочется иметь зависимости от дополнительных библиотек.
                                              +1
                                              Можно, конечно, вынести 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 );
                                              }
                                              
                            0
                            Пройдет ещё немного времени, и плюсовики совсем перестанут решать реальные проблемы, а будут думать, как же правильно заюзать все фичи языка с этим безумным синтаксисом. Здесь что-то не так. :)
                              0
                              Всё просто: чем сложнее задачи — тем сложнее инструмент.
                                –1
                                Задача «вертикально забить трубу в землю» сложнее, чем «забить гвоздь», но рабочие на стройке не пытаются собрать трубозабиватель на месте — они используют уже готовые, на которых работать не сильно сложнее, чем работать с молотком.
                                Инструментарий с задачами усложняется, но обычно не экспоненциально, как С++. :)
                                  +1
                                  Теперь представьте что вам нужно забить миллиард труб =)
                              0
                              Если суть задачи состоит в том, чтобы у 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'ам, задачу нужно было решить в другом виде.
                                0
                                Хотя, рантайм проверка на Вашем юнит-тесте проходит, если исправить строку
                                member = move_if_rr< T >( rr.Get() );
                                на
                                member = move_if_rr< T&& >( rr.Get() );
                                , что как раз укладывается в моем представлении. Ведь rr имеет тип T&&, а не T.
                                Сильно не ругайте, это мое видение постановки и решения задачи.
                                  0
                                  Даже если исправить 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 &&», а не «А &&»).
                                    0
                                    Касательно, названия 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»

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

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

                                Самое читаемое