The problem
Suppose we implement a string
class which represents, uhm, strings. We then want to add an operator+
which concatenates two string
s, and decide to implement that via expression templates to avoid multiple allocations when doing str1 + str2 + ... + strN
.
The operator will look like this:
stringbuilder<string, string> operator+(const string &a, const string &b)
stringbuilder
is a template class, which in turn overloads operator+
and has an implicit string
conversion operator. Pretty much the standard textbook exercise:
template<class T, class U> class stringbuilder;
template<> class stringbuilder<string, string> {
stringbuilder(const string &a, const string &b) : a(a), b(b) {};
const string &a;
const string &b;
operator string() const;
// ...
}
// recursive case similar,
// building a stringbuilder<stringbuilder<...>, string>
The above implementation works perfectly as long as someone does
string result = str1 + str2 + ... + strN;
However, it has a subtle bug. Assigning the result to a variable of the right type will make that variable hold references to all the strings that compose the expression. That means, for instance, that changing one of the strings will change the result:
void print(string);
string str1 = "foo";
string str2 = "bar";
right_type result = str1 + str2;
str1 = "fie";
print(result);
This will print fiebar, because of the str1 reference stored inside the expression template. It gets worse:
string f();
right_type result = str1 + f();
print(result); // kaboom
Now the expression template will contain a reference to a destroyed value, crashing your program straight away.
Now what's that right_type
? It is of course stringbuilder<stringbuilder<...>, string>
, i.e. the type the expression template magic is generating for us.
Now why would one use a hidden type like that? In fact, one doesn't use it explicitely -- but C++11's auto does!
auto result = str1 + str2 + ... + strN; // guess what's going on here?
The question
The bottom line is: it seems that this way of implementing expression templates (by storing cheap references instead of copying values or using shared pointers) gets broken as soon as one tries to store the expression template itself.
Therefore, I'd pretty much like a way of detecting if I'm building a rvalue or a lvalue, and provide different implementations of the expression template depending on whether a rvalue is built (keep references) or a lvalue is built (make copies).
Is there an estabilished design pattern to handle this situation?
The only things I was able to figure out during my research were that
One can overload member functions depending on
this
being an lvalue or rvalue, i.e.class C { void f() &; void f() &&; // called on temporaries }
however, it seems I can't do that on constructors as well.
In C++ one cannot really do ``type overloads'', i.e. offer multiple implementations of the same type, depending on how the type is going to be used (instances created as lvalues or rvalues).
right_type&
andright_type&&
andright_type const&
. I can blockright_type
, how do we block the others? Secret types? Will that blockauto
? – Prevailingstringbuilder
signature does perfect forwarding, not take-by-const&
. So yourstringbuilder<string, string>
is distinct fromstringbuilder<string const&, string const&>
-- one has two temporaries, the other has a pair ofconst&
s. Even if you don't store copies (and instead storestring&&
) you should do this, because this lets you know if you can move out of those temporaries or not. – Prevailingoperator string() &&;
cut it? Sure, someone could usemove
, but there's not anything more we can do. This problem has been unsolved for a very long time. – Savorauto
that nobody who I asked (including real experts) could give an answer to. :-( – Levantright_type&
andright_type const&
are doable if you make everything in sight private, butright_type&&
is indeed a problem. – Gillietteoperator auto
to the language :D – Savorright_type
to functions, which kind of defeats the idea. Nope, won't work. – Gillietteoperator X&()
andoperator X&&()
;) – Gillietteoperator X()
handles the second pretty closely. – Savorstd::move
to not compile for yourright_type
. Then, only allow/use rvalue member functions in yourright_type
(e.g. conversion tostring
viaoperator string() const&&
). This should disallow any use of lvalueright_type
objects andright_type&&
. – Coincidentalstd::forward<right_type>(instance)
(which you could also block), or if someone reimplementsstd::forward
orstd::move
from scratch for whatever insane reason. – Prevailing