
Yesterday I was cooking risotto for dinner and, as part of the process, I had to slice onions. That made me cry profusely, but nowhere near as much as when I’m dealing with slicing in C++.
What is slicing?
Slicing in C++ is what happens when you have a polymorphic type and you value-assign a base class to the value of a subclass. What I just wrote might even be imprecise, but even if it is 100% correct, wouldn’t be understandable by the most, so maybe an example will help:
struct A {
int x_;
A(int x) : x_(x) {}
};
struct B : A {
int y_;
B(int x, int y) : A(x), y_(y) {}
};
int main() {
B b{5, 3};
A a = b; // y is left behind
}
This behavior, in many cases, is undesirable, but that’s still the default behavior for C++.
Copies are problematic per se (only part of the object is copied), but with the introduction of move semantics, the problem worsened, because now it’s really hard to leave the object in a “valid but unspecified state” because the move constructor or assignment operator involved is that of the base class, and doesn’t know anything about the subclasses, and most of the times moved-from subclasses that deal with ownership are left in an invalid state, and thus end up leaking, crashing, or both.
So, how can we prevent it?
Prevent Slicing from one subclass
A first option is, explicitly forbidding slicing from a specific subclass:
struct B;
struct A {
int x_;
A(int x) : x_(x) {}
A(const B&) = delete; // prevent copy-construction
A& operator=(const B&) = delete; // prevent copy-assignment
A(B&&) = delete; // prevent move-construction
A& operator=(B&&) = delete; // prevent move-assignment
};
struct B : A {
int y_;
B(int x, int y) : A(x), y_(y) {}
};
int main() {
B b{5, 3};
A a = b; // Error!
}
This is doable, if we know exactly the subclasses we want to avoid to construct from, but in many (if not all) the cases, we really want to prevent slicing altogether. Also, forward-declaring subclasses, and explicitly deleting 2 constructors and 2 assignment operator for each of them is a tedious and error-prone process.
This isn’t gonna scale, let’s try to use generic programming to reduce the amount of lines to be repeated.
Prevent Slicing from one subclass, the generic way
We can try using something similar to CRTP, when instead of using the same type as template parameter, we use a forward-declared subclass of it.
template<typename T>
struct DontSliceFrom {
DontSliceFrom() = default;
DontSliceFrom(const T&) = delete; // prevent copy-construction
DontSliceFrom& operator=(const T&) = delete; // prevent copy-assignment
DontSliceFrom(T&&) = delete; // prevent move-construction
DontSliceFrom& operator=(T&&) = delete; // prevent move-assignment
};
struct B; // forward declaration of B
struct A : DontSliceFrom<B> {
int x_;
A(int x) : x_(x) {}
};
struct B : A {
int y_;
B(int x, int y) : A(x), y_(y) {}
};
int main() {
B b{5, 3};
A a = b; // Error?
}
Better? Not so fast. It doesn’t work. The error is, indeed, not generated, I imagine it’s because, when forwarding to the parent, the type is transformed into a A type.
The solution is to import the slice-disabling constructors into A:
template<typename T>
struct DontSliceFrom {
DontSliceFrom() = default;
DontSliceFrom(const T&) = delete; // prevent copy-construction
DontSliceFrom& operator=(const T&) = delete; // prevent copy-assignment
DontSliceFrom(T&&) = delete; // prevent move-construction
DontSliceFrom& operator=(T&&) = delete; // prevent move-assignment
};
struct B;
struct A : DontSliceFrom<B> {
int x_;
A(int x) : x_(x) {}
using DontSliceFrom<B>::DontSliceFrom; // <- This does the trick
};
struct B : A {
int y_;
B(int x, int y) : A(x), y_(y) {}
};
int main() {
B b{5, 3};
A a = b; // Error! (for real)
}
This is, again, repetitive, tedious, and error prone. It’s slightly better than before, but we still have to know and list all subclasses, twice. And anyway, this allows us to use DontSliceFrom with things that aren’t subclasses of A, even more potential error sources.
This isn’t gonna work.
Prevent slicing from all subclasses
Can we simply delete the copy/move contructor/assignment operator from anything that is a subclass of A? Let’s try this (C++20 version)
#include <concepts>
using namespace std;
struct A {
int x_;
A(int x) : x_(x) {}
A() = default;
A(const A&) = default;
A& operator=(const A&) = default;
A(A&&) = default;
A& operator=(A&&) = default;
A(const derived_from<A> auto&) = delete;
A& operator=(const derived_from<A> auto&) = delete;
A(derived_from<A> auto&&) = delete;
A& operator=(derived_from<A> auto&&) = delete;
};
struct B : A {
int y_;
B(int x, int y) : A(x), y_(y) {}
};
int main() {
B b{5, 3};
A a = b; // Error!
}
Why C++20 only? Because I’m lazy, and also because every time I have to use enable_if I spend 20 minutes staring at the documentation before remembering how it really works (and my brain usually explode like in the Lemming game from the 90s). Anyway, for those who can’t use C++20 yet, I asked ChatGPT to produce a C++17 version, which kinda worked (I just had to fix the style):
#include <type_traits>
#include <utility>
struct A {
int x_;
A(int x) : x_(x) {}
A() = default;
A(const A&) = default;
A& operator=(const A&) = default;
A(A&&) = default;
A& operator=(A&&) = default;
template <typename T, typename = std::enable_if_t<std::is_base_of_v<A, T> && !std::is_same_v<A, T>>>
A(const T&) = delete;
template <typename T, typename = std::enable_if_t<std::is_base_of_v<A, T> && !std::is_same_v<A, T>>>
A& operator=(const T&) = delete;
template <typename T, typename = std::enable_if_t<std::is_base_of_v<A, T> && !std::is_same_v<A, T>>>
A(T&&) = delete;
template <typename T, typename = std::enable_if_t<std::is_base_of_v<A, T> && !std::is_same_v<A, T>>>
A& operator=(T&&) = delete;
};
struct B : A {
int y_;
B(int x, int y) : A(x), y_(y) {}
};
int main() {
B b{5, 3};
A a = b; // Error!
}
Prevent slicing from all subclasses, now with CRTP!
Of course, we can now incorporate the idea we had previously (using CRTP, for real this time) to delete the slicing constructors:
#include <concepts>
using namespace std;
template <typename T>
struct DontSlice {
DontSlice() = default;
DontSlice(const derived_from<T> auto&) = delete;
DontSlice& operator=(const derived_from<T> auto&) = delete;
DontSlice(derived_from<T> auto&&) = delete;
DontSlice& operator=(derived_from<T> auto&&) = delete;
};
struct A : DontSlice<A> {
int x_;
A(int x) : x_(x) {}
A() = default;
A(const A&) = default;
A& operator=(const A&) = default;
A(A&&) = default;
A& operator=(A&&) = default;
using DontSlice<A>::DontSlice; // we still need this
};
struct B : A {
int y_;
B(int x, int y) : A(x), y_(y) {}
};
int main() {
B b{5, 3};
A a = b; // Error!
}
This works, finally, for any subclass. It’s a two-line-per-base-class solution, so not bad (compared to a four-line-per-subclass we had when we started)
The only drawback I see is that error reporting can be hard to read (look at godbolt link for the conceptualized solution, which is very terse in error reporting):
<source>: In function 'int main()':
<source>:34:11: error: use of deleted function 'A::A(const auto:1&) [with auto:1 = B][inherited from DontSlice<A>]'
34 | A a = b; // Error!
| ^
<source>:24:25: note: 'A::A(const auto:1&) [with auto:1 = B][inherited from DontSlice<A>]' is implicitly deleted because the default definition would be ill-formed:
24 | using DontSlice<A>::DontSlice;
| ^~~~~~~~~
<source>: At global scope:
<source>:24:25: error: use of deleted function 'DontSlice<T>::DontSlice(const auto:1&) [with auto:1 = B; T = A]'
<source>:8:5: note: declared here
8 | DontSlice(const derived_from<T> auto&) = delete;
| ^~~~~~~~~
<source>:24:25: note: use '-fdiagnostics-all-candidates' to display considered candidates
24 | using DontSlice<A>::DontSlice;
| ^~~~~~~~~
<source>: In function 'int main()':
<source>:34:11: note: use '-fdiagnostics-all-candidates' to display considered candidates
34 | A a = b; // Error!
| ^
Compiler returned: 1
Any other way?
I couldn’t find a one-line trick to make a class not sliceable, but only a two-liner (or a four-liner, with better error-reporting). Looks like I will still be crying when dealing with slicing of any kind, but at least in the future I’ll cry a bit less with C++ slicing (unfortunately this post isn’t gonna fix the slicing onion issue).
If you can find something better, I’m open to suggestions!
Credits: Image by Marco Costanzi from Pixabay
#
I would not trust code I hadn’t run through Clang-Tidy (no to mention –Wextra, –fsanitize=undefined,address, etc.). In this case, cppcoreguidelines-slicing is your friend: https://godbolt.org/z/Tjahhhsna
#
Very good point, I didn’t know there was such a clang-tidy check!
Nonetheless, I’m not worried about my own code (where a mistake would be caught by high warning levels, static analyzers, and tests in CI running with and without ASAN, UBSAN and TSAN), I’m worried about what some of the users of my libraries are capable of doing…