I started experimenting with CRTP and ran into a very interesting problem that has been keeping me awake for two days now.
Using CRTP I declare 3 structures: Column
, Header
and Footer
, which are inherited from Widget
.
The Column
structure takes the other widgets in the constructor and stores them in std::tuple, then in the Render()
method, calls the Render()
method of the widgets that are stored in std::tuple
.
In main()
you can see that the Header
widget's title field is set, but during debugging the Render()
method of the Column
widget I noticed that the title field looks at uninitialized memory, so the program ends up with a segfault.
The code uses standard c++20
.
Who can suggest what the problem is? Thanks in advance!
#include <iostream>
#include <tuple>
#include <string>
template<typename T>
struct Widget {
void Render() {
static_cast<T*>(this)->Render();
}
};
template<typename ...Args>
struct Column : public Widget<Column<Args...>> {
Column(Widget<Args>... args)
: children(std::forward<Widget<Args>>(args)...)
{}
void Render() {
std::cout << "Column {" << std::endl;
std::apply([](auto&... widgets){
auto render = [](auto& widget){
widget.Render();
};
(render(widgets), ...);
}, children);
std::cout << "}" << std::endl;
}
private:
std::tuple<Widget<Args>...> children;
};
struct Header : public Widget<Header> {
void Render() {
std::cout << "Header { title=" << this->title << " }" << std::endl;
}
std::string title;
};
struct Footer : public Widget<Footer> {
void Render() {
std::cout << "Footer {}" << std::endl;
}
};
template<typename T>
void AplicationRun(Widget<T> widget) {
widget.Render();
}
int main() {
auto app = Column{
Header{
.title = "Title"
},
Column{},
Footer{},
};
AplicationRun(app);
return 0;
}
I tried using move sematic in the Column
constructor but that didn't help.
The argument to the function
template<typename T>
void AplicationRun(Widget<T> widget) {
is a Widget<Column>
which is not a Column
. Unlike other languages you might be used to, this argument is not a reference to the object, but is in fact a whole object. To fit the Column
object into this Widget<Column>
argument the compiler only copies the Widget<Column>
part, a common pitfall called slicing because the argument is only a "slice" of the original object.
You could fix this by changing Widget<T>
to T
. However it would still make a copy of the Column
object, which I suppose you don't want.
To avoid making a copy you can pass by reference: Widget<T> &
or T &
. Either one is okay, because the Widget<T> &
parameter can be a reference to a Column
object.
If passing by reference, you can also choose to use Widget<T> const &
or T const &
which prevents the function from modifying the object (and the Render
function will need to be marked const
so you can still call it). You don't need that in this small program if you don't want to, but it's common for programmers on bigger programs to restrict themselves like this because it helps to avoid mistakes (bugs).