C++ Logo

std-proposals

Advanced search

Re: user copy constructor

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Thu, 12 Dec 2019 19:57:38 -0500
On Thu, Dec 12, 2019 at 5:06 PM Bengt Gustafsson via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> [...]
> The default word is to be interpreted as doing a meberwise copy
> construction for those members not explicitly mentioned, which is similar
> to how initialized member declarations or with this lacking default
> construction is used for unmentioned members today. So the word default
> indicates that instead of defaulting to initiation (or default
> construction) for unmentioned members copying/moving should be done. Of
> course it should be legalt to not mention any members for a copy
> constructor which is to copy all members but then in addition do something
> in the function body. This may be just as valuable as the original use case:
>
> Foo(const Foo&) : default { log("Copy constructed yet another Foo"); }
>

This exact scenario has come up in training course material I have used: we
have a class type with a bunch of fields, and then also it has some
relatively simple responsibility to discharge in the destructor, copy
constructor, and copy-assignment operator. It's a pain to write out all the
"default behavior" when all we want to do is add a single line for the
responsibility.

Grammar tangent: I initially imagined a fantasy syntax more like the
existing syntax for a pure virtual function with a body:
    Foo(const Foo&) = default { log("Copy constructed a Foo"); }
    Foo& operator=(const Foo&) = default { log("Assigned a Foo"); return
*this; }
However, this has at least two problems. First, we have to keep this
ill-formed:
    struct Foo {
        Foo& operator=(const Foo&) = default;
    };
    Foo& Foo::operator=(const Foo&) { log("Assigned a Foo"); return *this; }
An =default'ed declaration must remain a definition, and you mustn't be
able to go add an out-of-line definition later (as you can with a pure
virtual function).
Second, it's inconvenient that you can't say "my assignment is the default
assignment, just with this one extra line." You have to say at least "my
assignment is the default assignment, just with this one extra line, *and*
I return *this."

The right answer is what other people have been saying: The original sin
here is that we have a class type with a bunch of fields *and* a
responsibility to discharge in its special members. What we should have is
a class type with a bunch of fields plus one more field, where *that one
field* is in charge of the responsibility. That is, rather than

int books = 0;
struct Book {
    std::string title;
    std::string author;
    int pagecount;
    Book(const Book& rhs) : title(rhs.title), author(rhs.author),
pagecount(rhs.pagecount) { ++books; }
    Book& operator=(const Book&) = default;
    ~Book() { --books; }
};

what we should have instead is

int books = 0;
struct Book {
    struct Counter {
        Counter(const Counter&) { ++books; }
        ~Counter() { --books; }
    };
    std::string title;
    std::string author;
    int pagecount;
    Counter counter;
    Book(const Book&) = default;
    Book& operator=(const Book&) = default;
    ~Book() = default;
};

Single Responsibility Principle and RAII for the win. :)

Other responders have already shown how to apply the same logic to create a
"no-copy" data member.

HTH,
Arthur

Received on 2019-12-12 19:00:14