Page 1 of 1

C++ default operator= implementation

Posted: Sat Jul 13, 2013 11:46 am
by TheBuzzSaw
The more I work in C++, the more I've noticed a theme in my object design. To avoid repeating code, I often make a copy function and a destroy function. My copy constructor calls the copy function. My destructor calls the destroy function. My operator overload calls destroy followed by copy. Would this not be a superior default implementation?

This has crossed my mind due to my recent interest in language design. It would be so much nicer being able to simply define the copy constructor, move constructor, and the destructor. The = operator would (if not defined otherwise) simply call an in-place destructor (not release the memory, of course) and then call an in-place copy constructor. That would cover more use cases than the current mechanism of simply assigning each member variable in a 1-to-1 fashion. (That would be the copy constructor's default implementation.)

Are there situations I am overlooking that the current C++ standard would address that my proposed change would not? Methinks I am going to head this direction with K++. ^_^

Re: C++ default operator= implementation

Posted: Sat Jul 13, 2013 5:42 pm
by dandymcgee
I've read this a few times and still don't really understand what you're trying to say. Do you have a code example?

Re: C++ default operator= implementation

Posted: Sat Jul 13, 2013 11:20 pm
by Nokurn
TheBuzzSaw wrote:The more I work in C++, the more I've noticed a theme in my object design. To avoid repeating code, I often make a copy function and a destroy function. My copy constructor calls the copy function. My destructor calls the destroy function. My operator overload calls destroy followed by copy. Would this not be a superior default implementation?

This has crossed my mind due to my recent interest in language design. It would be so much nicer being able to simply define the copy constructor, move constructor, and the destructor. The = operator would (if not defined otherwise) simply call an in-place destructor (not release the memory, of course) and then call an in-place copy constructor. That would cover more use cases than the current mechanism of simply assigning each member variable in a 1-to-1 fashion. (That would be the copy constructor's default implementation.)

Are there situations I am overlooking that the current C++ standard would address that my proposed change would not? Methinks I am going to head this direction with K++. ^_^
I do not think you are understanding the distinction between initialization and assignment, whether it be by copy or move.

Let's say you do this:
class foo {
public:
    std::string bar;
    int baz;
};
foo f;
f is instantiated on the stack and then constructed using the implementation-provided default constructor, which default-constructs all foo class members. The std::string default constructor initializes an empty string. The int default constructor is nop, as int is a plain-old-data type. This is simple. Let's add a copy constructor and copy assignment operator that uses your idea.
class foo {
public:
    std::string bar;
    int baz;

    foo() { }

    foo(const foo& other)
    {
        copy(other);
    }

    foo& operator=(const foo& other)
    {
        copy(other);
        return *this;
    }

    void copy(const foo& other)
    {
        bar = other.bar;
        baz = other.baz;
    }
};
foo f;
foo g = f;
Now, we have a problem. f is being default-constructed, then g is being assigned to the copy of f. By using the assignment operator to initialize g, however, a compiler without move semantics will do this:
foo f;
foo g;
g = f;
This means that g is being default-constructed before being assigned. You're doing twice as much work by initializing the string member bar and then overwriting it with another string! This is no big deal with a C++11 compiler, but let's say we're using C++03. The obvious solution, then, is to switch from assignment to initialization:
foo f;
foo g(f);
Now we are using the copy constructor. This, however, is doing the same thing. The constructor is still initializing all class members not in the constructor's initializer list, before calling the copy method, so we haven't really done anything. What you are supposed to do is use member initialization:
foo(const foo& other) : bar(other.bar), baz(other.baz) { }
This prevents the compiler from automatically initializing bar and baz, and instead uses your own initialization.

As for your destroy function, I try to avoid putting myself in a situation where having zombie objects is normal. I prefer to tightly control the lifetime of the objects themselves, rather than the lifetime of their members, as I feel that this it the natural thing to do in C++. It's not necessary to use a destroy function unless you are using inheritance, as you can call destructors directly:
f.~foo();
However, if I see you doing this in any of your code, I will very quickly get the fuck out of there.

Also, move semantics in C++11 can help with many of the problems you are having, I think, by distinguishing between creating a true copy of an object and moving its data to another instance.

Edit: I am very tired right now, I might've misunderstood or explained something poorly. If you ask for clarification I will provide it when I am properly awake.

Re: C++ default operator= implementation

Posted: Sun Jul 14, 2013 2:24 pm
by bbguimaraes
I'm pretty sure
Foo g = f;
is still using the copy constructor. The rest of the post is right on, though.

P.S.: yeah, just tested it:
class C {
    public:
        C() {}
        C(const C & other);
        C & operator=(const C & other) {}
};

int main() {
    C c;
    C d = c;
}
$ g++ --version; g++ -o test test.cpp
g++ (Ubuntu/Linaro 4.6.4-1ubuntu1~12.04) 4.6.4
/tmp/ccn1INuJ.o: In function `main':
test.cpp:(.text+0x23): undefined reference to `C::C(C const&)'
collect2: ld returned 1 exit status

Re: C++ default operator= implementation

Posted: Sun Jul 14, 2013 4:47 pm
by TheBuzzSaw
@Nokurn, as was pointed out, you are incorrect.

Code: Select all

Foo f = g;
That is just syntactic sugar to invoke the copy constructor.

And trust me: I understand how initialization and assignment work in C++.

From there, I think you missed the point of my post. I am referring strictly to the compiler-provided default implementation. My point is that it provides an erroneous construct. I do not build my copy/destroy functions in all my objects. I was merely observing that it is a common pattern. Here is my point:

Code: Select all

class Stuff
{
public:
    Stuff(size_t size)
    {
        block = malloc(size);
    }

    Stuff(const Stuff& other)
    {
        size = other.size;
        block = malloc(size);
        memcpy(block, other.block, size);
    }

    ~Stuff()
    {
        free(block);
    }

private:
    void* block;
    size_t size;
};
So far, so good, right? The problem is that the default implementation does a member by member copy.

Code: Select all

Stuff& operator=(const Stuff& other)
{
    block = other.block;
    size = other.size;
    return *this;
}
Now we're hosed because the block will be double freed. My point is that a better default implementation would be this:

Code: Select all

Stuff& operator=(const Stuff& other)
{
    ~Stuff();
    new (this) Stuff(other);
    return *this;
}
While not optimal, it is infinitely more correct.

The same applies to the equivalent move semantics.

Re: C++ default operator= implementation

Posted: Mon Jul 15, 2013 4:53 pm
by Falco Girgis
Just a few things, gentlemen...
Nokurn wrote:Let's say you do this:
class foo {
public:
    std::string bar;
    int baz;
};
foo f;
f is instantiated on the stack and then constructed using the implementation-provided default constructor,
I'm just being anal, but if you're actually declaring that at global scope, f is being allocated in the data segment, not the stack.
Nokurn wrote:This means that g is being default-constructed before being assigned. You're doing twice as much work by initializing the string member bar and then overwriting it with another string! This is no big deal with a C++11 compiler, but let's say we're using C++03. The obvious solution, then, is to switch from assignment to initialization:
Move semantics only apply to temporary variables (r-values), so the scenario of initializing one variable from an existing variable would not even use move semantics in C++11.
Nokurn wrote: It's not necessary to use a destroy function unless you are using inheritance, as you can call destructors directly:
f.~foo();
However, if I see you doing this in any of your code, I will very quickly get the fuck out of there.
The placement destructor is actually a very useful construct when you're managing your own memory (especially in embedded environments)... You use placement new to initialize an existing address then use the placement delete to uninitialize the object without actually freeing the memory.

TheBuzzSaw, I see what you're saying... That actually does kinda make sense to me... and the default, compiler-generated copy constructor and default constructors would still make this work as expected for the normal scenarios...

Re: C++ default operator= implementation

Posted: Thu Jul 18, 2013 1:43 pm
by MarauderIIC
Falco Girgis wrote:
Nokurn wrote: It's not necessary to use a destroy function unless you are using inheritance, as you can call destructors directly:
f.~foo();
However, if I see you doing this in any of your code, I will very quickly get the fuck out of there.
The placement destructor is actually a very useful construct when you're managing your own memory (especially in embedded environments)... You use placement new to initialize an existing address then use the placement delete to uninitialize the object without actually freeing the memory.
I've never had a need to do placement new, but why not do it this way?
int main()
{
    {
        Foo* foo = new(0x1234) Foo();
        foo->doStuff();
    } /* Yay, destructor */
}

Re: C++ default operator= implementation

Posted: Thu Jul 18, 2013 2:27 pm
by Falco Girgis
^ that will not invoke the destructor. Your extra scope there will remove the pointer from the stack, not destroy what it's pointing to. That's a memory leak.

Re: C++ default operator= implementation

Posted: Thu Jul 18, 2013 5:29 pm
by MarauderIIC
I thought it looked like one, derp. I brainfarted and combined his non-pointer syntax with your stuff about placement new. embarrassing

Re: C++ default operator= implementation

Posted: Thu Jul 18, 2013 5:33 pm
by Falco Girgis
MarauderIIC wrote:I thought it looked like one, derp. I brainfarted and combined his non-pointer syntax with your stuff about placement new. embarrassing
lol you're fine. For a minute I thought that was legit too. :lol:

Re: C++ default operator= implementation

Posted: Thu Jul 18, 2013 5:55 pm
by Nokurn
Rereading this thread, I can see that I shouldn't have been posting after running a donation car wash in the Mojave Desert for 8 hours during July. ;)

I can see the merits of your suggestion, but my main qualm with this implementation is that it assumes that copy-assignment requires full destruction. It also assumes that copy-construction and copy-assignment are semantically equivalent, but that complaint has fewer consequences. I think your idea would mainly benefit people who write low-level classes. Those writing classes on the application level wouldn't see any real use for this. If your class consists only of object members (no raw pointers, no references), the standard default copy-constructor works perfectly, generating calls to the members' copy constructors, which may or may not destruct before copying, depending on the member's requirements, which are up to the author to determine and implement.

Re: C++ default operator= implementation

Posted: Fri Jul 19, 2013 1:22 pm
by TheBuzzSaw
Nokurn wrote:Rereading this thread, I can see that I shouldn't have been posting after running a donation car wash in the Mojave Desert for 8 hours during July. ;)

I can see the merits of your suggestion, but my main qualm with this implementation is that it assumes that copy-assignment requires full destruction. It also assumes that copy-construction and copy-assignment are semantically equivalent, but that complaint has fewer consequences. I think your idea would mainly benefit people who write low-level classes. Those writing classes on the application level wouldn't see any real use for this. If your class consists only of object members (no raw pointers, no references), the standard default copy-constructor works perfectly, generating calls to the members' copy constructors, which may or may not destruct before copying, depending on the member's requirements, which are up to the author to determine and implement.
This highlights an important point. There are two fundamental categories of class design: one where the members are self-contained (where direct assignment is safe) and one where the members represent something outside itself (where direct assignment is deadly). The reason I proposed the new implementation was to make sure that all cases would work in the absence of a superior implementation.

Re: C++ default operator= implementation

Posted: Fri Jul 19, 2013 3:31 pm
by Nokurn
TheBuzzSaw wrote:
Nokurn wrote:Rereading this thread, I can see that I shouldn't have been posting after running a donation car wash in the Mojave Desert for 8 hours during July. ;)

I can see the merits of your suggestion, but my main qualm with this implementation is that it assumes that copy-assignment requires full destruction. It also assumes that copy-construction and copy-assignment are semantically equivalent, but that complaint has fewer consequences. I think your idea would mainly benefit people who write low-level classes. Those writing classes on the application level wouldn't see any real use for this. If your class consists only of object members (no raw pointers, no references), the standard default copy-constructor works perfectly, generating calls to the members' copy constructors, which may or may not destruct before copying, depending on the member's requirements, which are up to the author to determine and implement.
This highlights an important point. There are two fundamental categories of class design: one where the members are self-contained (where direct assignment is safe) and one where the members represent something outside itself (where direct assignment is deadly). The reason I proposed the new implementation was to make sure that all cases would work in the absence of a superior implementation.
I can understand that, but I wouldn't be able to justify the amount of overhead that such a default implementation would incur. Of course, I can't make these claims with any certainty, but I would think there is more application-level code than low-level code in production. While the low-level classes that would benefit from this are likely already doing something similar, the higher level classes that use default-generated copy-assignment operators would suffer some overhead from explicitly calling all member destructors prior to copying, because everything would essentially be destroyed twice (once in the destructor, once in the low-level copy).

If C++ were a less general purpose language and more of a systems language, I think this would make a fine default implementation, provided that there would be no need for copy-construction and copy-assignment to be semantically distinct operations.