Copy, move, and assignment operators with a derived class

Started by
22 comments, last by Bregma 6 years, 6 months ago
35 minutes ago, matt77hias said:

I don't know why Intellisense gets mad

Intellisesne might be the wrong word here. Its whichever entity is responsible putting a red squiggly under things. The linter?

30 minutes ago, alvaro said:

 

You don't need to inherit to share functionality. Composition will be better in the vast majority of situations. So both VertexBuffer and ConstantBuffer can have a member of type Buffer (or MemoryBuffer, or RawBuffer... Using good names is important) and share functionality that way.

 

I kind of lied here, completely forgot what I was trying to do. I actually do have a spot where I could use a vector of Buffers. I have this factory called BufferModule which has methods like createVertexBuffer, createIndexBuffer, etc. All of those buffer classes (VertexBuffer, IndexBuffer, etc) are derived from the base Buffer. So in order to hold all of these I need a member variable like


std::vector<Buffer*> bufferObjects;

Cause if I were to just a normal vector of objects, my child buffer class  would be sliced when I would try to do something like this right?


//Vector of the parent class Buffer
std::vector<Buffer> bufferObjects;

//Put the child class VertexBuffer into the vector
VertexBuffer vertexBuffer;
graphicsDevice->device->CreateBuffer(&bufferDescription, NULL, &vertexBuffer.buffer);
buffers.push_back(vertexBuffer); // Not good... Just lost all of the info that was not specifically a part of the parent class

So to fix this I would need to do


//Vector of the parent class Buffer. Using a pointer to prevent the object slice
std::vector<Buffer*> bufferObjects;

VertexBuffer vertexBuffer = new VertexBuffer(); //Need to use new here otherwise when this goes out of scope all of our data goes bye-bye
graphicsDevice->device->CreateBuffer(&bufferDescription, NULL, &vertexBuffer.buffer);
buffers.push_back(&vertexBuffer); //Hurray! No object slicing happens

Only problem I have here is that I really don't like the use of the new keyword :(

Advertisement
1 hour ago, alvaro said:

 

You don't need to inherit to share functionality. Composition will be better in the vast majority of situations. So both VertexBuffer and ConstantBuffer can have a member of type Buffer (or MemoryBuffer, or RawBuffer... Using good names is important) and share functionality that way.

 

Note that private inheritance is one implementation of composition :P

🧙

4 minutes ago, matt77hias said:

Note that private inheritance is one implementation of composition

Yes, it's the one with the confusing syntax. :)

 

30 minutes ago, alvaro said:

Yes, it's the one with the confusing syntax. :)

And the confusing defaults. Why use public always as default for struct and use private always as default for class? This makes no sense for inheritance. But on the other hand if the syntax is not explicit, one asks for trouble sooner or later.

🧙

Types should usually either have value semantics or reference semantics, but not both. Value types can be used as reference types occasionally (what C# would consider "boxing"), but not vice versa.

A polymorphic assignment operator has properties of both and is definitely a code smell. Instead, try creating a reference type that can be owned by a value type. This gives you the best of both worlds, and allows you to use pImpl and other helpful patterns. The stricter semantic dichotomy also makes the code easier to work with and allows you to avoid a lot of these dark corners of C++ :)

5 hours ago, matt77hias said:

Shoudn't the argument be a reference for a general copy assignment operator?

No.  Trace through the operation of the code I wrote.  See how the copy constructor is used to implement the assignment operator?  Now, isn't it elegant?

Stephen M. Webb
Professional Free Software Developer

10 minutes ago, Bregma said:

No.  Trace through the operation of the code I wrote.  See how the copy constructor is used to implement the assignment operator?  Now, isn't it elegant?

Ok I get it. Elegant: yes.

But come on, you need to look at this few lines of code twice imho to get what is happening.

Someone of your team sees your signature. Adds the "missing" reference. And then it all depends on you "swp" signature, how things would go...

🧙

58 minutes ago, matt77hias said:

Ok I get it. Elegant: yes.

But come on, you need to look at this few lines of code twice imho to get what is happening.

Someone of your team sees your signature. Adds the "missing" reference. And then it all depends on you "swp" signature, how things would go...

I personally prefer the reference as well, since you can add an if(this != &other) before creating the temporary and swapping. Even though your swap function will have the same check, and it's semantically the same either way, no use creating a throwaway copy in that case :)

18 hours ago, matt77hias said:

Ok I get it. Elegant: yes.

But come on, you need to look at this few lines of code twice imho to get what is happening.

Someone of your team sees your signature. Adds the "missing" reference. And then it all depends on you "swp" signature, how things would go...

I do not work with bottom-grade programmers who would alter functionality without understanding it first. 

If you absolutely must blindly follow rigid ideology and make the argument to your copy assignment operator a reference, the code would look like this.


VertexBuffer& VertexBuffer::operator=(VertexBuffer const& other)
{
	VertexBuffer tmp(other);
	this->swap(tmp);
	return *this;
}

Interestingly, modern compilers will pessimize that alternative code because they can not take advantage of copy elision like they can with pass-by-value parameters.  Your coworker's blind obedience to cargo-cult pass-by-const-reference just cost a thousand CPU cycles.  Consider giving them an opportunity to pursue a more satisfying role as a node.js front-end developer where they can cause less harm.

One of the big advantages of the copy-and-swap idiom is that it provides the strong exception safety guarantee, whereas the original code did not (again, trace it through).  You might argue against the use of exceptions in C++ (after all, we're already speaking of a level of mindless reverence at play in your place of work) but writing the most robust code as possible is usually a win in the long run.  Sure, in this trivial example you're unlikely to see an exception, but not all code is so trivial and the copy-and-swap idiom will always apply.

As for adding a self-check, consider the cost of adding and maintaining an equality operator (and corresponding inequality operator) and the cost of calling it every time and compare that to how ofter you write code that performs self-assignment.  I know I very very rarely write code that assigns an object to itself, but code that is never written costs less and is guaranteed bug-free.  Always spending extra CPU cycles and increasing the risk in code to make a single highly unlikely edge-case more efficient seems like a lose-lose tradeoff to me.

Really, other than deleting the copy assignment operator entirely or adding debug printfs, why would you ever use anything other than the two-line copy-and-swap I wrote above for all your classes?

Stephen M. Webb
Professional Free Software Developer

15 minutes ago, Bregma said:

Really, other than deleting the copy assignment operator entirely or adding debug printfs, why would you ever use anything other than the two-line copy-and-swap I wrote above for all your classes?

I use for 95% of the cases:


VertexBuffer &VertexBuffer::operator=(const VertexBuffer &buffer) = delete;

and for 4.99% of the cases:


VertexBuffer &VertexBuffer::operator=(const VertexBuffer &buffer) = default;

 

🧙

This topic is closed to new replies.

Advertisement