C++ default operator= implementation

Whether you're a newbie or an experienced programmer, any questions, help, or just talk of any language will be welcomed here.

Moderator: Coders of Rage

Post Reply
User avatar
TheBuzzSaw
Chaos Rift Junior
Chaos Rift Junior
Posts: 310
Joined: Wed Dec 02, 2009 3:55 pm
Current Project: Paroxysm
Favorite Gaming Platforms: PC
Programming Language of Choice: C++
Contact:

C++ default operator= implementation

Post 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++. ^_^
User avatar
dandymcgee
ES Beta Backer
ES Beta Backer
Posts: 4709
Joined: Tue Apr 29, 2008 3:24 pm
Current Project: https://github.com/dbechrd/RicoTech
Favorite Gaming Platforms: NES, Sega Genesis, PS2, PC
Programming Language of Choice: C
Location: San Francisco
Contact:

Re: C++ default operator= implementation

Post 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?
Falco Girgis wrote:It is imperative that I can broadcast my narcissistic commit strings to the Twitter! Tweet Tweet, bitches! :twisted:
User avatar
Nokurn
Chaos Rift Regular
Chaos Rift Regular
Posts: 164
Joined: Mon Jan 31, 2011 12:08 pm
Favorite Gaming Platforms: PC, SNES, Dreamcast, PS2, N64
Programming Language of Choice: Proper C++
Location: Southern California
Contact:

Re: C++ default operator= implementation

Post 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.
User avatar
bbguimaraes
Chaos Rift Junior
Chaos Rift Junior
Posts: 294
Joined: Wed Apr 11, 2012 4:34 pm
Programming Language of Choice: c++
Location: Brazil
Contact:

Re: C++ default operator= implementation

Post 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
User avatar
TheBuzzSaw
Chaos Rift Junior
Chaos Rift Junior
Posts: 310
Joined: Wed Dec 02, 2009 3:55 pm
Current Project: Paroxysm
Favorite Gaming Platforms: PC
Programming Language of Choice: C++
Contact:

Re: C++ default operator= implementation

Post 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.
User avatar
Falco Girgis
Elysian Shadows Team
Elysian Shadows Team
Posts: 10294
Joined: Thu May 20, 2004 2:04 pm
Current Project: Elysian Shadows
Favorite Gaming Platforms: Dreamcast, SNES, NES
Programming Language of Choice: C/++
Location: Studio Vorbis, AL
Contact:

Re: C++ default operator= implementation

Post 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...
User avatar
MarauderIIC
Respected Programmer
Respected Programmer
Posts: 3406
Joined: Sat Jul 10, 2004 3:05 pm
Location: Maryland, USA

Re: C++ default operator= implementation

Post 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 */
}
I realized the moment I fell into the fissure that the book would not be destroyed as I had planned.
User avatar
Falco Girgis
Elysian Shadows Team
Elysian Shadows Team
Posts: 10294
Joined: Thu May 20, 2004 2:04 pm
Current Project: Elysian Shadows
Favorite Gaming Platforms: Dreamcast, SNES, NES
Programming Language of Choice: C/++
Location: Studio Vorbis, AL
Contact:

Re: C++ default operator= implementation

Post 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.
User avatar
MarauderIIC
Respected Programmer
Respected Programmer
Posts: 3406
Joined: Sat Jul 10, 2004 3:05 pm
Location: Maryland, USA

Re: C++ default operator= implementation

Post by MarauderIIC »

I thought it looked like one, derp. I brainfarted and combined his non-pointer syntax with your stuff about placement new. embarrassing
I realized the moment I fell into the fissure that the book would not be destroyed as I had planned.
User avatar
Falco Girgis
Elysian Shadows Team
Elysian Shadows Team
Posts: 10294
Joined: Thu May 20, 2004 2:04 pm
Current Project: Elysian Shadows
Favorite Gaming Platforms: Dreamcast, SNES, NES
Programming Language of Choice: C/++
Location: Studio Vorbis, AL
Contact:

Re: C++ default operator= implementation

Post 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:
User avatar
Nokurn
Chaos Rift Regular
Chaos Rift Regular
Posts: 164
Joined: Mon Jan 31, 2011 12:08 pm
Favorite Gaming Platforms: PC, SNES, Dreamcast, PS2, N64
Programming Language of Choice: Proper C++
Location: Southern California
Contact:

Re: C++ default operator= implementation

Post 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.
User avatar
TheBuzzSaw
Chaos Rift Junior
Chaos Rift Junior
Posts: 310
Joined: Wed Dec 02, 2009 3:55 pm
Current Project: Paroxysm
Favorite Gaming Platforms: PC
Programming Language of Choice: C++
Contact:

Re: C++ default operator= implementation

Post 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.
User avatar
Nokurn
Chaos Rift Regular
Chaos Rift Regular
Posts: 164
Joined: Mon Jan 31, 2011 12:08 pm
Favorite Gaming Platforms: PC, SNES, Dreamcast, PS2, N64
Programming Language of Choice: Proper C++
Location: Southern California
Contact:

Re: C++ default operator= implementation

Post 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.
Post Reply