48

I have deleted all the constructors, even then following code is working perfectly. How and why ?

class Ax
{    
    public:
    
    Ax() = delete;
    Ax(Ax const&)=delete;
    Ax(Ax&&)=delete;
    void operator=(Ax const&)=delete;
    void operator=(Ax&&)=delete;

    void print()
    {
        cout << "Hello \n";
    }
};

int main(int argc, char** argv) 
{           
    Ax{}.print();
    return 0;
}
virus00x
  • 713
  • 5
  • 12

2 Answers2

42

(For a thorough walk-through of this topic, see the blog article The fickle aggregate)


Aggregate initialization

Class Ax is an aggregate in C++11, C++14 and C++17, as it has no user-provided constructors, which means that Ax{} is aggregate initialization, bypassing any user-declared constructors, even deleted ones.

struct NonConstructible {
    NonConstructible() = delete;
    NonConstructible(const NonConstructible&) = delete;
    NonConstructible(NonConstructible&&) = delete;
};

int main() {
    //NonConstructible nc;  // error: call to deleted constructor

    // Aggregate initialization (and thus accepted) in
    // C++11, C++14 and C++17.
    // Rejected in C++20 (error: call to deleted constructor).
    NonConstructible nc{};
}

The definition of what is an aggregate class has changed through various standard versions (C++11 through C++20), and these rules can have somewhat surprising consequences. As of C++20, particularly due to the implementation of

most of the frequently surprising aggregate behaviour has been addressed, specifically by no longer allowing aggregates to have user-declared constructors, a stricter requirement for a class to be an aggregate than just prohibiting user-provided constructors.


User-provided or only user-declared explicitly-defaulted constructors

Note that providing an explicitly-defaulted (or deleted) definition out-of-line counts as a user-provided constructor, meaning that in the following example, B has a user-provided default constructor, whereas A does not:

struct A {
    A() = default; // not user-provided.
    int a;
};

struct B {
    B(); // user-provided.
    int b;
};

// Out of line definition: a user-provided
// explicitly-defaulted constructor.
B::B() = default;

with the result that A is an aggregate, whereas B is not. This, in turn, means that initialization of B by means of an empty direct-list-init will result in its data member b being left in an uninitialized state. For A, however, the same initialization syntax will result in (via aggregate initialization of the A object and subsequent value initalization of its data member a) zero-initialization of its data member a:

A a{};
// Empty brace direct-list-init:
// -> A has no user-provided constructor
// -> aggregate initialization
// -> data member 'a' is value-initialized
// -> data member 'a' is zero-initialized

B b{};
// Empty brace direct-list-init:
// -> B has a user-provided constructor
// -> value-initialization
// -> default-initialization
// -> the explicitly-defaulted constructor will
//    not initialize the data member 'b'
// -> data member 'b' is left in an unititialized state

This may come as a surprise, and with the obvious risk of reading the uninitialized data member b with the result of undefined behaviour:

A a{};
B b{};     // may appear as a sound and complete initialization of 'b'.
a.a = b.b; // reading uninitialized 'b.b': undefined behaviour.
dfrib
  • 70,367
  • 12
  • 127
  • 192
  • So `=delete` counts as user-provided? – Quimby Sep 29 '20 at 08:01
  • 2
    @Quimby No. Explicitly-deleted and explicitly-defaulted definitions provided _at first declaration_ does not count as user-provided. Providing these explicitly-defaulted definitions _out-of-line_ counts as user-provided, but a separate rule is that `= delete` may not be used out of line (ill-formed), with the result that `= delete` will never count as user-provided. [Demo](https://wandbox.org/permlink/fkyGvwhN0ztSaNdY). – dfrib Sep 29 '20 at 08:08
  • @dfri Well, that is what I thought too. But from the paper, p.8, the proposed wording is "no user-provided, explicit,user-declared or inherited constructors ", so `=delete` must count as user-provided since the next section of the proposal lists such example as " Not an aggregate". – Quimby Sep 29 '20 at 08:15
  • @dfri Reading more, the three examples at the bottom of p.8 also disallow `B()=default` as not agreggate in contradiction to your example, am I reading it wrongly? – Quimby Sep 29 '20 at 08:18
  • 3
    @Quimby My whole previous comment applies to what is used-provided. You are mixing topics w.r.t. the paper (P1008R1): the paper focuses on changing _what is an aggregate_ but not _what counts as user-provided_. In C++20 you can't even _declare_ a constructor for an aggregate class. This is the essence of the (accepted) proposal, that any class with user-declared ctors (deleted, defaulted of just not defined) should never be an aggregate. Note that also in C++20, both `Foo() = default;` and `Foo() = delete;` counts as user-provided, but this no longer has relevance for aggregates. – dfrib Sep 29 '20 at 08:19
12

In C++17, your example is an aggregate. For C++17 aggregates only need to have no user-provided constructors; user-declared (but explicitly deleted or defaulted) constructors are fine.

In this case, then, aggregate initialization is performed when you do Ax{}, which doesn't call any of the constructors... including the deleted ones, and so this compiles.

In C++20 the rules were changed so that any user-declared constructors prevent the type from being an aggregate, and so the example will fail to compile.

See also https://en.cppreference.com/w/cpp/language/aggregate_initialization

N. Shead
  • 3,828
  • 1
  • 16
  • 22