Last year I've discovered another surprising usecase while working on a two-way C++-to-JavaScript bindings.
It requires a combination of following conditions:
- You have a copyable and movable class
Base
.
- You have a non-copyable non-movable class
Derived
deriving from Base
.
- You really, really do not want an instance of
Base
inside Derived
to be movable as well.
- You, however, really want slicing to work for whatever reason.
- All classes are actually templates and you want to use template type deduction, so you cannot really use
Derived::operator const Base&()
or similar tricks instead of public inheritance.
#include <cassert>
#include <iostream>
#include <string>
#include <utility>
// Simple class which can be copied and moved.
template<typename T>
struct Base {
std::string data;
};
template<typename T>
struct Derived : Base<T> {
// Complex class which derives from Base<T> so that type deduction works
// in function calls below. This class also wants to be non-copyable
// and non-movable, so we disable copy and move.
Derived() : Base<T>{"Hello World"} {}
~Derived() {
// As no move is permitted, `data` should be left untouched, right?
assert(this->data == "Hello World");
}
Derived(const Derived&) = delete;
Derived(Derived&&) = delete;
Derived& operator=(const Derived&) = delete;
Derived& operator=(Derived&&) = delete;
};
// assertion fails when the `const` below is commented, wow!
/*const*/ auto create_derived() { return Derived<int>{}; }
// Next two functions hold reference to Base<T>/Derived<T>, so there
// are definitely no copies or moves when they get `create_derived()`
// as a parameter. Temporary materializations only.
template<typename T>
void good_use_1(const Base<T> &) { std::cout << "good_use_1 runs" << std::endl; }
template<typename T>
void good_use_2(const Derived<T> &) { std::cout << "good_use_2 runs" << std::endl; }
// This function actually takes ownership of its argument. If the argument
// was a temporary Derived<T>(), move-slicing happens: Base<T>(Base<T>&&) is invoked,
// modifying Derived<T>::data.
template<typename T>
void oops_use(Base<T>) { std::cout << "bad_use runs" << std::endl; }
int main() {
good_use_1(create_derived());
good_use_2(create_derived());
oops_use(create_derived());
}
The fact that I did not specify the type argument for oops_use<>
means that the compiler should be able to deduce it from argument's type, hence the requirement that Base<T>
is actually a real base of Derived<T>
.
An implicit conversion should happen when calling oops_use(Base<T>)
. For that, create_derived()
's result is materialized into a temporary Derived<T>
value, which is then moved into oops_use
's argument by Base<T>(Base<T>&&)
move constructor. Hence, the materialized temporary is now moved-from, and the assertion fails.
We cannot delete that move constructor, because it will make Base<T>
non-movable. And we cannot really prevent Base<T>&&
from binding to Derived<T>&&
(unless we explicitly delete Base<T>(Derived<T>&&)
, which should be done for all derived classes).
So, the only resolution without Base
modification here is to make create_derived()
return const Derived<T>
, so that oops_use
's argument's constructor cannot move from the materialized temporary.
I like this example because not only it compiles both with and without const
without any undefined behaviour, it behaves differently with and without const
, and the correct behavior actually happens with const
only.