struct wallet {
int m_id = 0;
int m_size = 0;
double* m_data = nullptr;
};The copy constructor is invoked when a new wallet object is created from an existing one.
wallet a(1, 10);
wallet b = a; // copy ctorwallet a(1, 10);
wallet b(a); // copy ctorwallet f(wallet w) {
return w;
}
wallet a(1, 10);
wallet b = f(a); // copy ctor into parameterwallet make() {
wallet t(7, 70);
return t; // elided or move ctor
}wallet a(1, 10);
auto lam = [a]() { }; // copy ctor into closurestd::vector<wallet> v;
wallet a(1, 10);
v.push_back(a); // copy ctorThe copy assignment operator is used when an already-existing object is overwritten.
wallet a(1, 10);
wallet b(2, 20);
b = a; // copy assignwallet a(1, 10);
a = a; // safe no-opstd::vector<wallet> v;
v.emplace_back(1, 10);
v.emplace_back(2, 20);
v[1] = v[0]; // copy assignThe move constructor creates a new object by stealing resources from an rvalue.
wallet a(1, 10);
wallet b = std::move(a); // move ctorwallet b = wallet(2, 20); // elided or move ctorwallet b = f(wallet(3, 30)); // move ctorstd::vector<wallet> v;
v.push_back(wallet(4, 40)); // move ctorMove assignment overwrites an existing object using an rvalue.
wallet a(1, 10);
wallet b(2, 20);
b = std::move(a); // move assignv[1] = std::move(v[0]); // move assign- ✅ Copy constructor → new object from lvalue
- ✅ Copy assignment → overwrite existing object
- ✅ Move constructor → new object from rvalue
- ✅ Move assignment → overwrite existing object from rvalue
If you define any one of the following, you should define all five:
- destructor
- copy constructor
- copy assignment
- move constructor
- move assignment
Otherwise, define none and rely on defaults.
noexcept is a promise: if an exception escapes, the program calls std::terminate(). So you add it when you are confident the function cannot throw, and when it improves behavior/performance (especially in the standard library).
- Why: Throwing from a destructor during stack unwinding is fatal (terminates). Also, standard library types assume destructors don’t throw.
- In your class,
delete[]does not throw, andstd::coutcan throw if exceptions are enabled on the stream, so if you keepstd::coutin destructors, it weakens the “can’t throw” guarantee.
Rule: In production code, keep destructors non-throwing and avoid throwing operations inside them. Mark them noexcept (or just default them).
- Why (big one): Containers like
std::vectorwill prefer moving during reallocation only if the move ctor isnoexcept(or if copying is not available). Otherwise they may copy to preserve the strong exception guarantee. - If your move is basically “steal pointers, null out rhs”, it’s naturally non-throwing.
- Why: Same reason: enables faster reallocation/moves and better container behavior.
- Also signals your move assignment is safe to use in many generic contexts.
- If you provide a
swap, make itnoexcept. It’s commonly used by algorithms and can affect optimizations.
They allocate memory (new[]). Allocation can throw std::bad_alloc.
So generally:
wallet(const wallet&)❌ notnoexceptwallet& operator=(const wallet&)❌ notnoexcept
(Unless you use a custom allocator or design that truly cannot throw, which is uncommon.)
Same reason: new can throw.
- The function does not allocate and does not call anything that can throw.
- It’s a move operation or destructor or swap for a type that might go into containers.
- You want to enable standard library optimizations and strong guarantees during reallocation.
- The function may allocate (
new,std::vectorgrowth, etc.). - It calls user-provided callbacks / virtual functions / code you don’t control.
- You’re not sure. (Because a wrong
noexceptturns exceptions into termination.)
wallet(wallet&&) noexcept;
wallet& operator=(wallet&&) noexcept;
~wallet() noexcept;wallet(const wallet&); // allocates
wallet& operator=(const wallet&); // allocates
wallet(int id, int size); // allocatesYes, with an important nuance:
- In modern C++ (since C++11), destructors are implicitly
noexcept(true)by default unless something in the destructor’s exception specification makes it potentially throwing. - So
~wallet()is effectively non-throwing by default.
But two practical points:
-
If an exception escapes a destructor, the program terminates anyway. So “implicitly noexcept” doesn’t mean “safe to throw”; it means “you must not let exceptions escape.”
-
If you put code inside the destructor that can throw (like I/O with exceptions enabled), then:
- Either the compiler may treat it as potentially throwing (depending on what you call),
- Or you still end up with
std::terminate()if an exception escapes.
Best practice:
- In teaching code, printing in destructors is fine.
- In production code, don’t do throwing work in destructors; keep them simple and
noexcept.
When std::vector<wallet> grows, it must relocate elements. It chooses between:
- move elements if
wallet’s move constructor isnoexcept - otherwise it may copy (slower) to preserve guarantees
So for performance and behavior in containers, this is the critical one:
✅ wallet(wallet&&) noexcept is often the difference between fast moves vs deep copies during vector growth.