5
\$\begingroup\$

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.

\$\endgroup\$
6
\$\begingroup\$

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 T1.

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.