Ch16.5: Derived-to-Base Conversion and Slicing
1. A Derived Object Is a Base Object
Because the base subobject is a real part of the derived object (Ch16.1), a derived object can be used wherever a base object is expected, with no cast:
| Target | From a derived lvalue |
What happens |
|---|---|---|
base& |
base& r = d; |
binds to the base subobject inside d; no copy |
base const& |
base const& r = d; |
same, read-only |
base* |
base* p = &d; |
points at the base subobject; no copy |
base |
base b = d; |
copies only the base part — this is slicing |
The first three are the useful ones: they are free, they preserve the object, and writes through the reference reach the original. The fourth is the trap, and it is the subject of this page.
2. Slicing: the Derived Part Is Thrown Away
base b = d; does not “convert” d; it constructs
a new base from the base subobject of d. The new object has
the size and the members of a base, so everything the derived class added
is gone — silently, with no warning, because for the compiler this is an ordinary
copy constructor call.
#include <fast_io.h>
#include <cstdint>
struct base
{
::std::int_least32_t v{};
};
struct derived : base
{
::std::int_least32_t extra{};
};
int main()
{
try
{
derived d{};
d.v = 1;
d.extra = 2;
base sliced = d; // copies only the base subobject
::fast_io::io::println("sliced.v=", sliced.v, " sizeof(sliced)=", sizeof(sliced));
base &ref = d; // refers to the base subobject inside d
ref.v = 5;
::fast_io::io::println("after ref.v=5: d.v=", d.v, " d.extra=", d.extra);
}
catch throws (::std::error e)
{
::fast_io::io::perrln(e);
}
}
Output:
sliced.v=1 sizeof(sliced)=4
after ref.v=5: d.v=5 d.extra=2
sizeof(sliced) is 4: the object is a base, not
a derived. ref, by contrast, is the real base subobject —
writing ref.v changed d.v, and d.extra is
untouched because it was never involved.
3. Where Slicing Happens Without You Writing It
Slicing is not an exotic operation. It happens whenever a derived object is copied into a base-typed value:
- A function parameter taken by value:
void f(base b); ... f(d);— the derived part is dropped inside the call. Takebase const&instead and nothing is lost. - A container of base values:
::fast_io::vector<base> v; v.push_back(d);— the vector holdsbase, so it storesbase. If you wanted the derived parts, you wanted a collection of something else: a variant, or an erased type (Ch15.10). - Assignment to a base object:
b = d;— only the base subobject is assigned. - Returning a derived object from a function whose return type is the base.
In a hierarchy with no virtual functions all of these are at least
type-honest: a base value really is a base, and
every operation on it is the base's. Once the base has virtual functions, slicing
becomes a logic bug as well, because the copy no longer behaves like the original
— Chapter 18 takes that up.
4. Passing by Reference, Not by Value
The rule that follows is one line long: a base class in a parameter list is a reference or a pointer, never a value.
inline void report(base const &b) throws; // uses the object you passed
inline void report(base b) throws; // uses a copy of its base subobject
inline void mutate(base &b) noexcept; // writes reach the original
This is also why the fast_io examples pass devices by reference:
::fast_io::io::println(out, ...) takes the output device by forwarding
reference, so an obuf_file stays an obuf_file and its
buffer is not copied (Ch11.1).
5. Rules of Thumb
- Take base classes as
B const&orB&. By-value base parameters are slicing machines. - Never store a derived object in a container of base values and expect the derived part back.
- If you need “many different derived things in one collection”, the
answer is not
vector<base>: it is a::std::variant, or the type-erasedshapeof Ch15.10. - A conversion to
base&is free and preserves identity; a conversion tobaseallocates a new object. When in doubt about which one happened, printsizeof. - Slicing is not an error the language reports. It is a copy, and copies are legal — which is exactly why it needs a rule.