| Lesson 4 | Copy Constructors |
| Objective | Understand the use of a copy constructor in C++ |
In C++23, the copy constructor remains a non-static member function of the form `T(const T&)` (or with cv-qualifiers and/or ref-qualifiers) that initializes a new object as a bit-for-bit or member-wise copy of an existing object of the same type. The language continues to implicitly declare a copy constructor when no user-defined copy constructor, move constructor, copy assignment, move assignment, or destructor exists, though such implicit generation is deprecated since C++11 if certain user-declared special members are present. Guaranteed copy elision rules introduced in C++17 and unchanged in C++23 eliminate many observable calls to copy constructors during return-by-value, initialization from temporaries, and throw expressions, allowing objects to be constructed directly in their final storage location. C++23's deducing this feature (explicit object parameters) enables more flexible and uniform member function definitions, though it is rarely applied directly to copy constructors themselves since they conventionally take a const lvalue reference parameter rather than using an explicit `this`. Overall, modern C++23 code benefits from stronger move semantics, mandatory elision, and concepts for better constraints, making copy constructors less frequently invoked in performance-critical paths while preserving their classical role when deep copying or explicit control is required.
A copy constructor builds a new object by copying from an existing object of the same type. It defines what it means to “clone” an object. In its conventional form:
Type::Type(const Type& other);
If you do not define a copy constructor, the compiler may generate one that performs a memberwise copy: each member is copied in order. This is correct for many “value-like” classes, but it is often wrong when the class owns resources (like dynamically allocated memory via a raw pointer).
Suppose we want to count occurrences of a character in a stack without changing the original stack. One classic approach is to pass the stack by value. That creates a local copy inside the function, so we can pop from the copy freely.
int cnt_char(char c, ch_stack s) {
int count = 0;
while (!s.empty()) {
count += (c == s.pop()); // pop from the local copy
}
return count;
}
count += (c == s.pop());
s.pop() removes and returns the top element of the local copy of the stack.c. The expression (c == s.pop()) is true or false.true converts to 1 and false converts to 0, so count increments by 1 only when the characters match.
The key idea: because s is passed by value, the function must create a local copy of the argument.
Creating that local copy requires a copy constructor (unless the type is trivially copyable or the copy is optimized away).
If ch_stack stores characters, then pop() should return a char, not a char*.
The one-liner can be expanded like this:
char just_popped = s.pop();
if (c == just_popped) {
++count;
}
Copy construction happens in these common situations:
Type b(a); or Type b = a;f(Type x) creates a copy of the argument (conceptually).Type make() may copy or move a local object into the return value (often optimized).If you pass an object by reference, you operate on the original object—no copy constructor is invoked:
void print_line2(String& a)
{
cout << a;
a = "\n";
cout << a;
}
String name("Fred");
print_line2(name); // no copy; modifies name directly
cout << name; // value is now changed
The most important modern rule is the Rule of Zero:
if your class uses RAII members (like std::string, std::vector, std::unique_ptr),
you usually do not need to write a destructor, copy constructor, or copy assignment operator at all.
The compiler-generated versions will be correct.
If your class manually owns resources (for example, it stores a raw char* that it deletes),
then you typically need the Rule of Three (copy constructor, copy assignment, destructor),
and in modern C++ the Rule of Five adds move constructor and move assignment.
The following pattern illustrates why a custom copy constructor is needed when a class owns a buffer.
In modern C++ you should use std::string instead, but this is a good learning example:
Copy Constructor
ClassName::ClassName(const ClassName& parameter) {
statements
}
Example:
String::String(const String& right)
{
len = right.length();
buffer = new char[len + 1];
for (int i = 0; i < len; ++i)
buffer[i] = right[i];
buffer[len] = '\0';
}
The compiler-generated copy constructor performs memberwise copy. This is exactly what you want when:
But if your class uses manual dynamic allocation, a memberwise copy typically produces shallow copy semantics (two objects owning the same pointer), which is unsafe. In those cases, define copying explicitly—or modernize the class to use RAII so you can return to the Rule of Zero.
const& when you want to avoid copying large objects.std::string, std::vector) so copy behavior is correct automatically.