Mehrwolf
Mehrwolf

Reputation: 8527

User-defined move constructor for member of lvalue reference type

I'm playing around with move semantics on a compiler, which has rvalue references but does not support defaulted move constructors. I would like to generate something like the wrapper class below, which works even if the template parameter is an lvalue reference. However, this straightforward approach does not compile because it tries to initialize an int& from an int.

#define USER_DEFINED   0

template <typename T>
struct Wrapper
{
    Wrapper(T t)
        : m_t(t)
    {
    }

    Wrapper(const Wrapper&) = delete;
    Wrapper& operator=(const Wrapper&) = delete;

#if USER_DEFINED
    Wrapper(Wrapper&& w)
        : m_t(std::move(w.m_t))
    {
    }
#else
    Wrapper(Wrapper&&) = default;
#endif

private:
    T m_t;
};

int main()
{
    int i = 0;
    Wrapper<int&> w1 = Wrapper<int&>(i);
    Wrapper<std::string> w2 = Wrapper<std::string>("text");
}

The obvious solution would be to have two move constructors, one for lvalue references and one for all other types. Something like this for example:

template <typename U = T>
Wrapper(typename std::enable_if<!std::is_lvalue_reference<U>::value, Wrapper>::type&& w)
    : m_t(std::move(w.m_t))
{
}

template <typename U = T>
Wrapper(typename std::enable_if<std::is_lvalue_reference<U>::value, Wrapper>::type&& w)
    : m_t(w.m_t)
{
}

So is this the way to go? Maybe the expression inside the enable_if<> should be more generic? Or can I use something different from std::move() and have one single constructor for all types?

Upvotes: 2

Views: 724

Answers (1)

5gon12eder
5gon12eder

Reputation: 25409

Okay, here is a solution that I think will work as you want it but I must admit that I don't fully understand how it does so.

The most important change I've made was to replace std::move with std::forward<T> in the move constructor. I have also added a move assignment operator but here is the thing I don't understand: Unless everywhere else, it needs std::move instead of std::forward! Finally, I have also added a std::forward to your constructor that accepts a T so it doesn't make two copies of its argument. It is actually required that we accept the T by value and use std::forward here. Overloading for const T& and T&& would fail if T is a reference because then T&& would also collapse to an lvalue reference and the overload become ambiguous.

#include <iostream>
#include <utility>

template <typename T>
class Wrapper
{

public:

  Wrapper(T t) : m_t {std::forward<T>(t)}
  {
  }

  Wrapper(Wrapper&& w) : m_t {std::forward<T>(w.m_t)}
  {
  }

  Wrapper(const Wrapper&) = delete;

  Wrapper&
  operator=(Wrapper&& w)
  {
    if (this != &w)
      this->m_t = std::move(w.m_t);
    return *this;
  }

  Wrapper&
  operator=(const Wrapper&) = delete;

private:

  T m_t;
};

Now, let's take it for a test drive with an instrumented type that allows us see what is going on.

struct A
{
  A ()
  {
    std::cerr << "A was default-constructed" << std::endl;
  }

  A (const A&)
  {
    std::cerr << "A was copy-constructed" << std::endl;
  }

  A (A&&)
  {
    std::cerr << "A was move-constructed" << std::endl;
  }

  A&
  operator=(const A&)
  {
    std::cerr << "A was copy-assigned" << std::endl;
    return *this;
  }

  A&
  operator=(A&&)
  {
    std::cerr << "A was move-assigned" << std::endl;
    return *this;
  }

  ~A ()
  {
    std::cerr << "A was destroyed" << std::endl;
  }
};

int main()
{
  A a {};
  Wrapper<A> val1 {a};
  Wrapper<A> val2 {std::move(val1)};
  val1 = std::move(val2);
  Wrapper<A&> ref1 {a};
  Wrapper<A&> ref2 {std::move(ref1)};
  ref1 = std::move(ref2);
}

Compiled with GCC 4.9.1 (Where the whole exercise is actually rather pointless because it supports all kinds of moves already out of the box.) and all optimizations turned off, the output is as follows (comments added).

A was default-constructed  ; automatic variable a in main
A was copy-constructed     ; copied as Wrapper<A>'s constructor argument
A was move-constructed     ; forwarded in Wrapper<A>'s initializer
A was destroyed            ; the moved-away from copy as the constructor returns
A was move-constructed     ; forwarded in Wrapper<A>'s move-constructor
A was move-assigned        ; move-assignment operator in Wrapper<A>
A was move-assigned        ; not sure about this one... (apparently caused by the Wrapper<A&> move-assignment)
A was destroyed            ; the sub-object of val2
A was destroyed            ; the sub-object of val1
A was destroyed            ; the automatic variable a in main

Upvotes: 4

Related Questions