4

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?

Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • Just try and see what doesn't give you a compiler error. I would use `A(A const&)` if both work – Daniel May 29 '11 at 12:39
  • @Dani i cannot do that. it's a template. – Johannes Schaub - litb May 29 '11 at 12:39
  • So instantiate it once and copy it just to make the compiler compile the template – Daniel May 29 '11 at 12:40
  • Both are "correct" copy constructors, are you asking for a way of replicating the compiler logic for just which one it uses for an implicit declaration if didn't supply any constructors? – CB Bailey May 29 '11 at 12:43
  • @Charles updated with explanation. – Johannes Schaub - litb May 29 '11 at 12:54
  • Have you ever seen any compiler that doesn't declare the automatically generated copy ctor as `A const&`? – Xeo May 29 '11 at 13:09
  • @Xeo I think that breaks down to the question "have you ever seen a program being compiled in a c++ compiler that has a class with a "auto_ptr" member and no explicit copy constructor?". Any class that has a member or base with no copy ctor of type `(C const &)` or `(C const volatile&)` has a non-const copy ctor parameter. – Johannes Schaub - litb May 29 '11 at 13:15
  • @Dani: I think the issue is that `A`'s implicit copy ctor takes a const or non-const parameter according to whether T's declared or implicit copy ctor takes a const or non-const parameter. So instantiating the template once doesn't help - if you declare it const in order to default it, then you're fine when `T` is `int` or `string`, or any other type with well-behaved value semantics, but not fine when T is `auto_ptr` or some annoying user-defined class. – Steve Jessop May 29 '11 at 14:09
  • 1
    I was wondering (but Johannes, you are here the expert) if it is possible to write a type trait to detect whether the copy constructor takes a non-const reference, and then use the result of that evaluation to define one or the other copy constructor signatures. – David Rodríguez - dribeas May 29 '11 at 14:10
  • @David perhaps with `is_constructible::value` ? Haven't tried. – Johannes Schaub - litb May 29 '11 at 14:20
  • Two points: (a) I don't get it; (b) I didn't expect to. – Lightness Races in Orbit May 29 '11 at 15:34

3 Answers3

4

I guess I fail to see the problem... how does this differ from the common case of implementing a copy constructor?

If your copy constructor will not modify the argument (and the implicitly defined copy constructor will not do it) then the argument should be passed as a constant reference. The only use case I know of for a copy constructor that does not take the argument by constant reference is when in C++03 you want to implement moving a la std::auto_ptr which is usually a bad idea anyway. And in C++0x moving would be implemented as you have with a move constructor.

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
  • the `=default` shall have the same signature (i.e., argument type) as the compiler generated one. If for some odd case a compiler generates the copy ctor as `A&`, then `A(A const&) = default` won't work. – Xeo May 29 '11 at 13:08
  • 1
    @Xeo: which will only take a non-const reference if any of the member objects or subclasses of the class for which the copy constructor is being implicitly declared takes the argument by non-const reference. That being rare in general for user defined classes (I only know of `auto_ptr` that does it, and it's brought more problems than anything). Now, if you use a non-const reference in your class, it will work for all versions of `T`, at the cost of breaking many potentially sane use cases: `void foo( A const & x ) { A tmp = x; ... }` – David Rodríguez - dribeas May 29 '11 at 13:23
  • ... including that it will force the signature of copy constructors for all containing or deriving classes to be non-const reference, forcing the same issues to the users. – David Rodríguez - dribeas May 29 '11 at 13:25
2

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.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • for why SFINAE doesn't apply, see http://stackoverflow.com/q/2892087/34509 . A quick and dirty fix would be `template Copyable(Copyable& rhs, typename std::enable_if::type* = 0): ...`. – Johannes Schaub - litb May 29 '11 at 15:29
  • @Johannes: thanks, it does the trick. However only special members may be defaulted, so adding that little SFINAE snippet prevents defaulting :/ – Matthieu M. May 29 '11 at 16:16
1

I've never seen a case where the implicit copy constructor would be A&- you should be good in any case with const A&.

Puppy
  • 144,682
  • 38
  • 256
  • 465
  • In a reference counted environment you need to modify the original to let it know there is one more reference – Daniel May 29 '11 at 13:06
  • @Dani: You yourself can always defne a `A&` copy ctor, but @DeadMG is talking about compiler generated versions. – Xeo May 29 '11 at 13:07
  • @Dani: That's what `const_cast` is for. – Puppy May 29 '11 at 13:09
  • @Dani: And yet, `shared_ptr` defines the constructor as `shared_ptr(shared_ptr const &)` The caveat there might be that the reference count is held by pointer, and the `const` only guarantees that the pointer will not be modified. If you are using a form of intrusive reference counting that would be another issue. But note that a class (with no move constructor) that takes the argument by non-const reference in the copy constructor cannot be used in STL containers. So my guess is that in that circumstance, the count would be `mutable`. I have neither think it out nor read any implementation. – David Rodríguez - dribeas May 29 '11 at 13:11
  • Or making the variable `mutable`. – Xeo May 29 '11 at 13:11
  • @Xeo, @Dani: At first I had the same reaction: `const_cast`!?!? But on second thoughts it might make sense. If you make the reference count `mutable` you are disabling the checks on *all* `const` member functions, while if you add a single `const_cast` in the *only* two fucntions that need it (*copy constructor*, *destructor*) then the compiler will be able to flag invalid modifications in the rest of the methods. – David Rodríguez - dribeas May 29 '11 at 13:32
  • @David: Double notification doesn't work. ;) Other than that, yeah, valid point about mutable. – Xeo May 29 '11 at 13:34
  • @DeadMG using `const_cast` means UB as soon as a user copies from a const instance. It really is a job for `mutable`. – Luc Danton May 29 '11 at 14:38