Best style for deleting all constructors (or other function)?
Asked Answered
A

3

11

Let's say I want to make a type that cannot be constructed (don't ask why).

struct Impossible
{

I could do it like this:

    Impossible() = delete;
    // disable automatically generated constructors, don't declare any others
    Impossible(const Impossible&) = delete;
    // I suppose this is redundant with nothing to copy

or like this:

    Impossible(...) = delete;
    // explicitly disable all constructors

or like this:

    template<typename... Ts>
    Impossible(Ts...) = delete;
    // explicitly disable all constructors, template version
};

I guess I could ask the same thing about any function, not just constructors.

Does it make any difference which one I choose? In terms of syntax I think I like the second option. But is there any situation, whatsoever, where it's possible to detect a difference (other than in the text of an error message)?

Avictor answered 3/1, 2013 at 11:17 Comment(0)
F
7

There is a difference, for example:

#include <type_traits>

struct Unconstructible
{
    Unconstructible() = delete;
};

static_assert( !std::is_default_constructible<Unconstructible>::value, "not default constructible" );
static_assert( std::is_copy_constructible<Unconstructible>::value, "copyable" );

Although you can never construct this object, so in practice you can never create a copy of one either, according the language and the type traits in the library, it is technically CopyConstructible, because there's an implicitly-declared copy constructor.

Similarly, with the Impossible(...) or Impossible(Ts&&...) forms there is still an implicitly-declared copy constructor.

On the other hand, if you do it this way:

#include <type_traits>

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

static_assert( !std::is_default_constructible<Unconstructible>::value, "not default constructible" );
static_assert( !std::is_copy_constructible<Unconstructible>::value, "not copyable" );

The existence of a user-declared constructor suppresses the implicit declaration of the default constructor, so the class is neither DefaultConstructible nor CopyConstructible.


N.B. your last example should probably be Impossible(Ts&&...) to match any types, including non-copyable and non-movable ones.

Floydflss answered 3/1, 2013 at 15:33 Comment(3)
Wait. Even if I explicitly declare a more-general constructor that meets the requirements for being a copy constructor (it accepts an argument of the self-type), and delete it, the compiler will still generate /another/ copy constructor, which is a better match after considering overloading? Is that definitely right?Avictor
Yes. [class.copy]/2 A non-template constructor for class X is a copy constructor if its first parameter is of type X&, const X&, volatile X& or const volatile X&, and either there are no other parameters or else all other parameters have default arguments. [class.copy]/7 If the class definition does not explicitly declare a copy constructor, one is declared implicitly. So the more-general constructor is not technically a "copy constructor" even though it can be used to copy the type.Floydflss
That's good to know. Thanks! (And mea culpa regarding Ts.... For some reason I was thinking that the types would be deduced to be anything (reference, rvalue reference, whatever) as necessary depending on what was passed, but upon reflection that's obviously dumb. Just because it's a parameter pack won't make it magically behave differently from single parameters. (As opposed to rvalue references to template parameters which do behave magically in a similar sort of way to enable perfect forwarding, as you said.))Avictor
E
8

The first one is more than enough - no constructors will be generated by the compiler and, most imporatantly, it's idiomatic.

Excrescency answered 3/1, 2013 at 11:18 Comment(0)
F
7

There is a difference, for example:

#include <type_traits>

struct Unconstructible
{
    Unconstructible() = delete;
};

static_assert( !std::is_default_constructible<Unconstructible>::value, "not default constructible" );
static_assert( std::is_copy_constructible<Unconstructible>::value, "copyable" );

Although you can never construct this object, so in practice you can never create a copy of one either, according the language and the type traits in the library, it is technically CopyConstructible, because there's an implicitly-declared copy constructor.

Similarly, with the Impossible(...) or Impossible(Ts&&...) forms there is still an implicitly-declared copy constructor.

On the other hand, if you do it this way:

#include <type_traits>

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

static_assert( !std::is_default_constructible<Unconstructible>::value, "not default constructible" );
static_assert( !std::is_copy_constructible<Unconstructible>::value, "not copyable" );

The existence of a user-declared constructor suppresses the implicit declaration of the default constructor, so the class is neither DefaultConstructible nor CopyConstructible.


N.B. your last example should probably be Impossible(Ts&&...) to match any types, including non-copyable and non-movable ones.

Floydflss answered 3/1, 2013 at 15:33 Comment(3)
Wait. Even if I explicitly declare a more-general constructor that meets the requirements for being a copy constructor (it accepts an argument of the self-type), and delete it, the compiler will still generate /another/ copy constructor, which is a better match after considering overloading? Is that definitely right?Avictor
Yes. [class.copy]/2 A non-template constructor for class X is a copy constructor if its first parameter is of type X&, const X&, volatile X& or const volatile X&, and either there are no other parameters or else all other parameters have default arguments. [class.copy]/7 If the class definition does not explicitly declare a copy constructor, one is declared implicitly. So the more-general constructor is not technically a "copy constructor" even though it can be used to copy the type.Floydflss
That's good to know. Thanks! (And mea culpa regarding Ts.... For some reason I was thinking that the types would be deduced to be anything (reference, rvalue reference, whatever) as necessary depending on what was passed, but upon reflection that's obviously dumb. Just because it's a parameter pack won't make it magically behave differently from single parameters. (As opposed to rvalue references to template parameters which do behave magically in a similar sort of way to enable perfect forwarding, as you said.))Avictor
G
5

You cannot create instance of this class:

struct Impossible
{
    Impossible() = delete;
};

Note the question of copy-constructor (or move-constructor) doesn't even arise, so deleting them is unnecessary. An object cannot exist because it cannot be constructed, so the copying or moving is out of question.

Godthaab answered 3/1, 2013 at 11:21 Comment(1)
Yep, as I mentioned. But I'm curious about the "any possible way to detect" part of it. Is there any code you can write that will behave differently depending on which style I chose? Say, using SFINAE?Avictor

© 2022 - 2024 — McMap. All rights reserved.