I'm writing a templatized container and wanted to use an universal reference for the Add(T&& object)
function. Check out isocpp if you need an update on your universal/rvalue references. A quick example from that page states that, since the class is templatized, T&&
is fully specified and therefore a rvalue, not an universal reference.
template <class T, class Allocator = allocator<T> >
class vector {
public:
...
void push_back(T&& x); // fully specified parameter type ⇒ no type deduction;
... // && ≡ rvalue reference
};
This means that I cannot pass lvalues to the push_back function above.
vector<T> list;
list.Add(T()); // rvalue, compiles fine.
T data;
list.Add(data); // lvalue, does not compile.
In the case of std::vector this is solved by having a push_back(const T& object)
overload.
However, since the code in my Add
function is quite complex, I didn't want to copy it over.
However, if I am to templatize my add function, it does become universal reference. So I came up with the following code.
#include <iostream>
#include <type_traits>
#include <vector>
using namespace std;
template <class T>
class Container
{
public:
template <class UR = T>
void Add(UR&& object)
{
static_assert(std::is_same<std::remove_cv_t<std::remove_reference_t<UR>>, T>::value, "UR and T should be the same!");
m_Vector.push_back(std::forward<UR>(object));
}
std::vector<T> m_Vector;
};
class Data
{
public:
int x;
int y;
};
int main() {
Container<Data> list;
Data d;
list.Add(d);
return 0;
}
I've added the static assert to make sure that people do not actually templatize the Add
function. Does anyone know a cleaner solution, since this feels a bit of template magic? Should I follow the std method and just copy my add function (which is a bit more complex than this minimal example)? Can anyone come up with a case where my solution will not work?
Here is a small ideone version for you to play around with.
1 Answer 1
Your solution is principally correct. If you want to play nice with potential generic traits (at the cost of a potentially uglier error message), you'd replace the static_assert
with SFINAE:
template <
class UR = T,
class TypeMustBeT = std::enable_if_t<std::is_same<std::remove_reference_t<UR>, T>::value>
>
void Add(UR&& object)
{
m_Vector.push_back(std::forward<UR>(object));
}
This way, the function Add
will not exist at all when UR
is not a forwarding reference for T
1.
If you don't like the "template magic" involved in this and/or the compiler errors generated from misuse, you can solve this by providing the two strongly-typed overloads as std
does and forward to a common private implementation:
void Add(const T& object)
{
AddImpl(object);
}
void Add(T&& object)
{
AddImpl(std::move(object));
}
private:
template <class UR>
void AddImpl(UR&& object)
{
static_assert(std::is_same<std::remove_cv_t<std::remove_reference_t<UR>>, T>::value, "UR and T should be the same!");
m_Vector.push_back(std::forward<UR>(object));
}
This way, you can keep the static_assert
-using version and keep the static assertion just to check your code's correctness, not the client code's.
Note that the static_assert
needs to do std::remove_cv
after std::remove_reference
, otherwise it will fail for UR = const T&
. The same applies to the original code in the question too.
Unrelated to the question at hand, but I assume using namespace std;
is for this reduced example only and is not present in your real code. It's very bad practice to put that into header files in real code.
1 Note that "forwarding reference" is the preferred term instead of "universal reference," since it captures the semantics better.