5

In short, why the code below behaves like described in the comments?

struct A
{
    A() = delete;
    //A(const A&) {} // uncommenting this...
};

int main()
{
    A a{}; // ... breaks this
    //A(); // this fails in either case because of `A() = delete;` only
}

What part of the standard (or at least a page on cppreference) should I look at to understand this?

However, writing A(const A&) = default; instead of //A(const A&) {} doesn't break A a{};. What about this? I think the underlying cause is the same, but a word from who really knows C++ is better than what I think.

Enlico
  • 23,259
  • 6
  • 48
  • 102

2 Answers2

10

Without the user-provided copy constructor, A is an aggregate. Yes, even though we deleted the default constructor. It's something that was addressed in C++20.

So, prior to C++20, A a{}; is aggregate initialization, and so doesn't use the deleted constructor. When you uncomment the copy constructor, A stops being an aggregate, thus turning the aggregate initialization into value initialization. So the initialization of a will attempt to call the deleted constructor.

To divine the meaning of an initializer from the standard, one typically starts at [dcl.init]/16. Going through the bullets, one can find how the properties of the initializer (when matched with the properties of the types in question) will affect the way initialization occurs.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • 1
    I think this alone is enough the justify C++'s reputation of being only expert-friendly. – StoryTeller - Unslander Monica Jan 13 '21 at 07:58
  • I think this comment of yours can be loudly put as the headline of your answer (and I challenge you to do so! Ahahah :P). – Enlico Jan 13 '21 at 08:05
  • I suppose a word needed to say whether `without user-defined copy constructor` includes deleted copy constructor or not. Directly deleted one is considered as existing user-defined constructor (it exist! but deleted) – Swift - Friday Pie Jan 13 '21 at 08:17
  • @StoryTeller-UnslanderMonica, I've made a little add up to my question. Would you spend a word on that too? – Enlico Jan 13 '21 at 08:18
  • @Enlico we just both thought about that at same time? `A(const A&) = default;` makes constructor equivalent to one of aggregate – Swift - Friday Pie Jan 13 '21 at 08:20
  • 1
    @Enlico - I can't say I entirely appreciate moving the goal posts like that. But either way, it's the exact same thing. It's no longer a user-provided copy constructor, so it doesn't interfere with the aggregate status of `A`. – StoryTeller - Unslander Monica Jan 13 '21 at 08:20
  • 1
    @Swift-FridayPie, actually no. I mis-read your comment first (I read _without user-defined constructor_), which I didn't understand, and thought "what happens if I default construct the copy ctor?". – Enlico Jan 13 '21 at 08:22
  • I believe I didn't _move_ anything, but I _added_ something which I can't see how far from what originally was in the question is. So ok, @StoryTeller-UnslanderMonica, sorry for adding that bit, but still... – Enlico Jan 13 '21 at 08:25
  • 1
    @Enlico that's two sides of same coin which have tail on both ends. Both `A() = delete; A(const A&) = delete; ` and `A() = delete; A(const A&) = default; ` would compile in C++17, and won't in C++20. – Swift - Friday Pie Jan 13 '21 at 08:26
  • @Enlico [The fickle aggregate](https://dfrib.github.io/the-fickle-aggregate/) article covers this topic in detail. The TLDR is that C++20 finally sorted out a lot of the confusion with aggregates, with the stronger requirement that an aggregate class may not have _any user-declared_ constructors; meaning even explicitly-defaulted and explicitly-deleted constructors disqualify a class (as of C++20) from being an aggregate. – dfrib Jan 13 '21 at 09:34
  • @dfrib which really had put sticks into wheels of upgrading code from GNU C++98 \C++11 and of some frameworks\libraries – Swift - Friday Pie Jan 13 '21 at 21:24
3

Aggregate initialization is not value initialization

If a given class, say S, is an aggregate, then

S s{};

is aggregate initialization, not value initialization, and will bypass any constructors of S, even if they are deleted or private. The result of aggregate initialization is typically value-initialization of the (public(1)) data members of the aggregate.

(1) As shown below, a class with any private data member is never an aggregate class, not through any of the C++11 through C++20 standards.


The definition of what is an aggregate class is governed by:

where the typical confusion with aggregates is the weaker requirement of C++11 through C++17 to not have any user-provided(2) constructors, which was made much stricter in C++20 with the requirement to not have any user-declared constructors.

(2) The definition of what counts as user-provided function is governed by [dcl.fct.def.default]/4 in C++11 and [dcl.fct.def.default]/5 in C++14 through C++20; the definition of user-provided is essentially the same in all these language versions.

There are also other changes to the definition of aggregates between C++11, C++14 and C++17 that are not as well-known, but that likewise add to the aggregate confusion prior to C++20; we'll walk through a couple of examples to highlight them.


Is the example an aggregate?

For each example below; if the class S of the example is an aggregate (for a particular language version), the following is well-formed:

S s{};

The aggregate rules in all the examples above likewise applies for any kind of constructor (given that they are otherwise applicable, e.g. allows to be explicitly defaulted or deleted), not just S().


struct S {};
  • C++11: yes
  • C++14: yes
  • C++17: yes
  • C++20: yes

struct S {
    int x;
};
  • C++11: yes
  • C++14: yes
  • C++17: yes
  • C++20: yes

struct S {
    int x{42};
};
  • C++11: no (has a default member initializer)
  • C++14: yes
  • C++17: yes
  • C++20: yes

class S {
    int x;
};
  • C++11: no
  • C++14: no
  • C++17: no
  • C++20: no

Has a private data member.


struct S {
    S() = default;
};
  • C++11: yes
  • C++14: yes
  • C++17: yes
  • C++20: no (has a user-declared constructor)

struct S {
 private:
    S() = default;
};
  • C++11: yes
  • C++14: yes
  • C++17: yes
  • C++20: no (has a user-declared constructor)

struct S {
    S() = delete;
};
  • C++11: yes
  • C++14: yes
  • C++17: yes
  • C++20: no (has a user-declared constructor)

struct S {
    S();
};
S::S() = default;
  • C++11: no (has a user-provided constructor)
  • C++14: no (...)
  • C++17: no (...)
  • C++20: no (has a user-declared constructor)

An out of line explicitly-defaulted definition counts as user-provided; recalling [dcl.fct.def.default]/4 (C++11) / [dcl.fct.def.default]/5 (C++14, C++17):

A function is user-provided if it is user-declared and not explicitly defaulted or deleted on its first declaration.


struct S {
    explicit S() = default;
};
  • C++11: yes
  • C++14: yes
  • C++17: no (has an explicit constructor)
  • C++20: no (has a user-declared constructor)

C++17 adds the requirement that an aggregate may not have an explicit constructor.


struct S {
    int x{42};
    S() = delete;
};
  • C++11: no (has a default member initializer)
  • C++14: yes
  • C++17: yes
  • C++20: no (has a user-declared constructor)

C++14 removed the requirement of C++11 for an aggregate to have no default member initializers.


struct S {
    int x{42};
    explicit S() = delete;
};
  • C++11: no (has a default member initializer)
  • C++14: yes
  • C++17: no (has an explicit constructor)
  • C++20: no (has a user-declared constructor)
dfrib
  • 70,367
  • 12
  • 127
  • 192