Search code examples
c++c++11defaulted-functions

How can one default a special member function if one doesn't know its parameter types?


Consider this case:

template<typename T>
struct A {
  A(A ???&) = default;
  A(A&&) { /* ... */ }
  T t;
};

I explicitly declared a move constructor, so I need to explicitly declare a copy constructor if I want to have a non-deleted copy constructor. If I want to default it, how can I find out the correct parameter type?

A(A const&) = default; // or
A(A &) = default; // ?

I'm also interested in whether you encountered a case where such a scenario actually popped up in real programs. The spec says

A function that is explicitly defaulted shall ...

  • have the same declared function type (except for possibly differing ref-qualifiers and except that in the case of a copy constructor or copy assignment operator, the parameter type may be "reference to non-const T", where T is the name of the member function’s class) as if it had been implicitly declared,

If the implicitly-declared copy constructor would have type A &, I want to have my copy constructor be explicitly defaulted with parameter type A &. But if the implicitly-declared copy constructor would have parameter type A const&, I do not want to have my explicitly defaulted copy constructor have parameter type A &, because that would forbid copying from const lvalues.

I cannot declare both versions, because that would violate the above rule for the case when the implicitly declared function would have parameter type A & and my explicitly defaulted declaration has parameter type A const&. From what I can see, a difference is only allowed when the implicit declaration would be A const&, and the explicit declaration would be A &.

Edit: In fact, the spec says even

If a function is explicitly defaulted on its first dec- laration, ...

  • in the case of a copy constructor, move constructor, copy assignment operator, or move assignment operator, it shall have the same parameter type as if it had been implicitly declared.

So I need to define these out-of-class (which I think doesn't hurt, since as far as I can see the only difference is that the function will become non-trivial, which is likely anyway in those cases)

template<typename T>
struct A {
  A(A &);
  A(A const&);
  A(A&&) { /* ... */ }
  T t;
};

// valid!?
template<typename T> A<T>::A(A const&) = default;
template<typename T> A<T>::A(A &) = default;

Alright I found that it is invalid if the explicitly declared function is A const&, while the implicit declaration would be A &:

A user-provided explicitly-defaulted function (i.e., explicitly defaulted after its first declaration) is defined at the point where it is explicitly defaulted; if such a function is implicitly defined as deleted, the program is ill-formed.

This matches what GCC is doing. Now, how can I achieve my original goal of matching the type of the implicitly declared constructor?


Solution

  • It seems to me that you would need some type deduction (concepts ?).

    The compiler will generally use the A(A const&) version, unless it is required by one of the member that it is written A(A&). Therefore, we could wrap some little template hackery to check which version of a copy constructor each of the member has.

    Latest

    Consult it at ideone, or read the errors by Clang after the code snippet.

    #include <memory>
    #include <type_traits>
    
    template <bool Value, typename C>
    struct CopyConstructorImpl { typedef C const& type; };
    
    template <typename C>
    struct CopyConstructorImpl<false,C> { typedef C& type; };
    
    template <typename C, typename T>
    struct CopyConstructor {
      typedef typename CopyConstructorImpl<std::is_constructible<T, T const&>::value, C>::type type;
    };
    
    // Usage
    template <typename T>
    struct Copyable {
      typedef typename CopyConstructor<Copyable<T>, T>::type CopyType;
    
      Copyable(): t() {}
    
      Copyable(CopyType) = default;
    
      T t;
    };
    
    int main() {
      {
        typedef Copyable<std::auto_ptr<int>> C;
        C a; C const b;
        C c(a); (void)c;
        C d(b); (void)d;  // 32
      }
      {
        typedef Copyable<int> C;
        C a; C const b;
        C c(a); (void)c;
        C d(b); (void)d;
      }
    }
    

    Which gives:

    6167745.cpp:32:11: error: no matching constructor for initialization of 'C' (aka 'Copyable<std::auto_ptr<int> >')
            C d(b); (void)d;
              ^ ~
    6167745.cpp:22:7: note: candidate constructor not viable: 1st argument ('const C' (aka 'const Copyable<std::auto_ptr<int> >')) would lose const qualifier
          Copyable(CopyType) = default;
          ^
    6167745.cpp:20:7: note: candidate constructor not viable: requires 0 arguments, but 1 was provided
          Copyable(): t() {}
          ^
    1 error generated.
    

    Before Edition

    Here is the best I could come up with:

    #include <memory>
    #include <type_traits>
    
    // Usage
    template <typename T>
    struct Copyable
    {
      static bool constexpr CopyByConstRef = std::is_constructible<T, T const&>::value;
      static bool constexpr CopyByRef = !CopyByConstRef && std::is_constructible<T, T&>::value;
    
      Copyable(): t() {}
    
      Copyable(Copyable& rhs, typename std::enable_if<CopyByRef>::type* = 0): t(rhs.t) {}
      Copyable(Copyable const& rhs, typename std::enable_if<CopyByConstRef>::type* = 0): t(rhs.t) {}
    
      T t;
    };
    
    int main() {
      {
        typedef Copyable<std::auto_ptr<int>> C; // 21
        C a; C const b;                         // 22
        C c(a); (void)c;                        // 23
        C d(b); (void)d;                        // 24
      }
      {
        typedef Copyable<int> C;                // 27
        C a; C const b;                         // 28
        C c(a); (void)c;                        // 29
        C d(b); (void)d;                        // 30
      }
    }
    

    Which almost works... except that I got some errors when building the "a".

    6167745.cpp:14:78: error: no type named 'type' in 'std::enable_if<false, void>'
          Copyable(Copyable const& rhs, typename std::enable_if<CopyByConstRef>::type* = 0): t(rhs.t) {}
                                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
    6167745.cpp:22:11: note: in instantiation of template class 'Copyable<std::auto_ptr<int> >' requested here
            C a; C const b;
              ^
    

    And:

    6167745.cpp:13:67: error: no type named 'type' in 'std::enable_if<false, void>'
          Copyable(Copyable& rhs, typename std::enable_if<CopyByRef>::type* = 0): t(rhs.t) {}
                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
    6167745.cpp:28:11: note: in instantiation of template class 'Copyable<int>' requested here
            C a; C const b;
              ^
    

    Both occurs for the same reason, and I do not understand why. It seems that the compiler tries to implement all constructors even though I have a default constructor. I would have thought that SFINAE would apply, but it seems it does not.

    However, the error line 24 is correctly detected:

    6167745.cpp:24:11: error: no matching constructor for initialization of 'C' (aka 'Copyable<std::auto_ptr<int> >')
            C d(b); (void)d;
              ^ ~
    6167745.cpp:13:7: note: candidate constructor not viable: 1st argument ('const C' (aka 'const Copyable<std::auto_ptr<int> >')) would lose const qualifier
          Copyable(Copyable& rhs, typename std::enable_if<CopyByRef>::type* = 0): t(rhs.t) {}
          ^
    6167745.cpp:11:7: note: candidate constructor not viable: requires 0 arguments, but 1 was provided
          Copyable(): t() {}
          ^
    

    Where we can see that the CopyByConstRef was correctly evicted from the overload set, hopefully thanks to SFINAE.