Monday, March 28, 2011

Right value references and confusion with constructors

One of the most exciting new feature arriving with C++2011 is the right value reference. But if you want to have exact signatures for the definitions of your overloaded generic functions, you will probably end up with a lot of boiler code. However, as N2027 explains, it is possible to use a right value reference of a type parameter to accept any type. Here is an example:

template class T, class A1>
std::shared_ptr<T>
factory(A1&& a1)
{
  return std::shared_ptr<T>(new T(std::forward<a1>(a1)));
}

For this function that is not intended to be overloaded, it is satisfying. And it comes to become very practical to make expression templates.

template <typename T, typename U>
MyExprTempl<typename std::decay<T>::type,
            typename std::decay<U>::type>
expr(T&& t, U&& u) {
return
  MyExprTempl<typename std::decay<T>::type,
              typename std::decay<U>::type>
  { std::forward<T>(t), std::forward<U>(u) };
}

Moreover, expression templates typically have members of the parameter types (here T and U). So we can use the same trick on the constructor.

template <typename T, typename U>
struct MyExprTempl {
  T t;
  U u;
  template <typename RT, typename RU>
  MyExprTempl(RT&& t, RU& ru):
    t{std::forward<RT>(t)},
    u{std::forward<RU>(u)} {}
  //...
};

The you have an expression template with perfect forwarding all the way.

But if you try this, you will get problems when constructors will have only one parameter, even if you use keyword explicit. To show the problem, we can use the example with factory. Suppose the following class definition:

template <typename T>
struct A {
  std::shared_ptr<T> a;
  A() = delete;
  A(const A&) = default;
  template <typename U>
  explicit A(U&& u):
    a{factory<T>(std::forward<U>(u))} {}
  };
  //...
  A<int> a{0};
  A<int> b{a};

The first instantiation will work. It will call A::A(U&&) with (U = int). However, the second will try to call explicitly A::A(A&). There are two choices, either A::A(U&&) (U = A&) which does not need any conversion (A& && is equivalent to A&), or A(const A&), which needs a an implicit conversion from A& to const A&. Of course the compiler will choose the one we did not want. The technique was too permissive. We wanted such a constructor:

template <typename U,
          typename = typename
          std::enable_if<std::is_explicitly_convertible
              <U&&, T>::value>::type
   >
explicit A(U&& u):
  a{factory<T>(std::forward<U>(u))} {}

Since factory, calls the construction of a T explicitly, we have to allow even explicit conversions. We should also avoid is_constructible as this type traits expects a class. Here we could have a non-class type. We have to note that we have to check if U&& is convertible to T, as U&& is always the result type of std::forward<U>, and also the same on std::forward<T> within factory, so we know that it will always be U&& that will be used for the conversion.

No comments:

Post a Comment