Search code examples
c++stlstdvector

Is it possible to implement two push_back(..) methods of std::vector in a single universal reference method?


I'm trying to implement std::vector all by myself for some exercising purposes. My question is about the push_back(..) methods. There are two overloads of this method as in the following.

void push_back(const value_type& value);
void push_back(value_type&& value);

At first, I implemented them in two different functions. For the former one, I choose to copy construct the new element with the given value. And for the latter one, I choose to move construct the new element with the given value. Here are the implementations of mine:

template<class T>
void Vector<T>::push_back(const value_type& value)
{
    if(size() == capacity())    // Size is about to surpass the capacity
        grow(nextPowerOf2(capacity()), true);   // Grow and copy the old content

    new(data + sz++) value_type(value); // Copy construct new element with the incoming one
}

template<class T>
void Vector<T>::push_back(value_type&& value)
{
    if(size() == capacity())    // Size is about to surpass the capacity
        grow(nextPowerOf2(capacity()), true);   // Grow and copy the old content

    new(data + sz++) value_type(std::move(value)); // Move construct new element with the incoming
}

After then, I realized that I could merge these two functions in a single method that takes a universal reference such as in the following code:

template<class T>
template<class U>
void Vector<T>::push_back(U&& value)
{
    if(size() == capacity())    // Size is about to surpass the capacity
        grow(nextPowerOf2(capacity()), true);   // Grow and copy the old content

    // Construct new element with the incoming
    new(data + sz++) value_type(std::forward<U>(value));
}

I think this single method handles all the things that I was trying to do with two different methods. Is there a thing that I'm missing? Will this operation bother me later? I don't seek backward compatibility, so you can omit it.


Solution

  • The second version is not equivalent to the first one.

    In the first you take an argument of type "reference to T", and in the second U can be of any type, not related to T.

    So the second version is actually more an emplace_back than a push_back. It will take any value and try to construct an instance of T using that (though normally emplace_back takes a pack of arguments, for more flexibility).

    You could restrict U to a type compatible with T using SFINAE, but then the code might become more complicated than just having two separate overloads.

    template<class T>
    template<class U, typename std::enable_if_t<std::is_convertible_v<std::decay_t<U>, T>, int> = 0>
    void Vector<T>::push_back(U&& value)
    {
        // . . .
    

    stdlibc++ for example has two separate overloads for vector::push_back, but there push_back(T&&) simply delegates to emplace_back. So maybe you could do the same.