ValeriyKarasikov
ValeriyKarasikov

Reputation: 51

Why can't the constructor be called explicitly?

I was looking into creating a constructor to initialize an encapsulated std::array and came across a problem that the constructor of a copyable type (class A) could not be called explicitly. There are two cases in the given code. A simple example: an object of class B cannot explicitly call the copy constructor for a field of class A. And a complex one: the initialization of each element of the std::array requires an implicit call to the copy constructor. How can I avoid implicitly calling a copy constructor to initialize class members?

#include <cstddef> // std::size_t
#include <utility> // std::forward
#include <array> // std::array

struct A {
  explicit A() = default;
  explicit A(A&) = default; // ERROR: used implicitly when initializing 'b'
};

struct B {
  //explicit B(A& a) : a_{a} {} // partially solves the problem, but cannot be defaulted
  A a_;
};

template<class T, std::size_t N>
struct C {
  template<class... Ts>
  explicit C(Ts&&... il) : values_{std::forward<Ts>(il)...} {} // how to use 'explicit A(A&)'?
  std::array<T, N> values_;
};

int main() {
  A a;
  B b{a}; // basic problem
  C<A, 3> c{a, a, a}; // extended problem
  return 0;
}

Upvotes: 0

Views: 163

Answers (2)

JaMiT
JaMiT

Reputation: 16853

You can avoid implicitly calling a copy constructor by explicitly calling it. More commonly, this would be called explicitly making a copy, as in using A{a} to explicitly create a (temporary) copy of a.

B b{A{a}}; // basic problem
//  ^^ ^

For the template, I'll add a warning!

You can use the same technique when constructing an array, but first consider if you should. Adding generic code to handle a special case (when T is A) can have unforeseen consequences (when T is not A). Before modifying the constructor of C<>, you should find out why the copy constructor of A is explicit. One possibility is that copying A is supported, but expensive, hence each copy needs to be explicitly approved. Is this a valid place to approve copying A? Is it a valid place to approve copying any type with an explicit copy constructor?

Rather than modifying the template, it might be better to give A a (non-explicit) move constructor, which is another way to address an expensive-to-copy type. With a move constructor, you could apply the technique inside the main function, where you know you are dealing with A objects (and not some other type that should not be implicitly copied).

struct A {
  explicit A() = default;
  explicit A(A&) = default;
  A(A&&) = default;  // <--- Added a move constructor
};

C<A, 3> c{A{a}, A{a}, A{a}}; // extended problem
//        ^^ ^  ^^ ^  ^^ ^

Still, for completeness, if you are dead-set on making the template work, the technique can be applied there (no need to make explicit copies in main when initializing c).

// I don't recommend this, but it does compile:
explicit C(Ts&&... il) : values_{T{std::forward<Ts>(il)}...} {} // how to use 'explicit A(A&)'?
//                               ^^                    ^

Why does this work?

Isn't there still a copy from the temporary? Not as of C++17. Explicitly making a copy results in a prvalue, and a prvalue is not yet a materialized object. So when your A objects are initialized from these prvalues, the prvalues are materialized directly in the A objects; there is no additional copy. It's as if the prvalue tells the compiler how to construct an object, while the variable name tells the compiler where to construct it. In B b{A{a}}, the prvalue is A{a}, which explicitly says to make a copy, while the b names the place where the copy is to be made.

This is the same mechanism that gave us guaranteed copy elision (when returning a temporary object from a function).

Upvotes: 0

David G
David G

Reputation: 96810

You can use a pointer to do this:

struct B {
  std::unique_ptr<A> a_;
};

template<class T, std::size_t N>
struct C {
  template<class... Ts>
  explicit C(Ts&&... il) : values_{std::make_unique<A>(std::forward<Ts>(il))...} {}
  std::array<std::unique_ptr<T>, N> values_;
};

int main() {
  A a;
  B b{std::make_unique<A>(a)};
  C<A, 3> c{a, a, a};
}

Upvotes: -1

Related Questions