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.