Search code examples
c++templatesc++17moveperfect-forwarding

std::forward and rvalue references in a class


I've been reading about std::forward and I think I understand it well, but I don't think I understand it well enough to use it proficiently enough.

I have a template class that implements a container and has a method called insert. I wanted this method to accept constant references and rvalue references so that if the inserted element is a rvalue reference, it is moved into the container, not copied. For this, I first overloaded the insert method like this:

template <typename U>
void do_some_work(U&& x) noexcept
{
    m_data = std::forward<U>(x);
}
void insert(const T& x) 
{
    do_some_work(x);
}
void insert(T&& x) {
    do_some_work(std::forward<T>(x);
}

The problem is, these two functions now have to call an "inner" function that implements the assertion. If the functions are small, I guess this is not a problem, but if they are large, it is best to use templates, like this

template <typename U>
void insert(U&& x) 
{
    do_some_work(std::forward<U>(x);
}

QUESTION 1: Is this correct?


Now, I want to do the same but with std::vector.

void insert_vector(const std::vector<T>& v) noexcept {
    for (std::size_t i = 0; i < v.size(); ++i) {
        do_some_work(v[i]);
    }
}
void insert_vector(std::vector<T>&& v) noexcept 
{
    for (std::size_t i = 0; i < v.size(); ++i) 
    {
        do_some_work(std::forward<T>(v[i]));
    }
}

QUESTION 2: How do I collapse the insert_vector functions into a single one so that the call to do_some_work is done with

  • rvalue references when the vector is a rvalue reference (specified with std::move, for example),
  • constant reference when the vector is not given as an rvalue reference, like in (4) of the MWE below. The following does not work for me
template <typename U>
void insert_vector(U&& v) noexcept 
{
    for (std::size_t i = 0; i < v.size(); ++i) 
    {
        do_some_work(std::forward<T>(v[i]));
    }
}

Here is a minimum working example:

#include <iostream>
#include <vector>

template <typename T>
class my_class 
{
private:
    T m_data;

private:
    template <typename U>
    void do_some_more_work(U&& x) noexcept {
        m_data = std::forward<U>(x);
    }
    template <typename U>
    void do_some_work(U&& x) noexcept {
        do_some_more_work(std::forward<U>(x));
    }

public:
    void insert(const T& x) noexcept {
        std::cout << __PRETTY_FUNCTION__ << '\n';
        do_some_work(x);
    }
    void insert(T&& x) noexcept {
        std::cout << __PRETTY_FUNCTION__ << '\n';
        do_some_work(std::forward<T>(x));
    }
    void insert_vector(const std::vector<T>& v) noexcept {
        std::cout << __PRETTY_FUNCTION__ << '\n';
        for (std::size_t i = 0; i < v.size(); ++i) {
            do_some_work(v[i]);
        }
    }
    void insert_vector(std::vector<T>&& v) noexcept {
        std::cout << __PRETTY_FUNCTION__ << '\n';
        for (std::size_t i = 0; i < v.size(); ++i) {
            do_some_work(std::forward<T>(v[i]));
        }
    }
};

struct my_struct {
    my_struct() noexcept = default;
    my_struct(int v) noexcept : m_v(v) { }
    my_struct(const my_struct& s) noexcept {
        std::cout << __PRETTY_FUNCTION__ << '\n';
        m_v = s.m_v;
    }
    my_struct(my_struct&& s) noexcept {
        std::cout << __PRETTY_FUNCTION__ << '\n';
        m_v = std::move(s.m_v); // not need, but whatever
        s.m_v = -1;
    }
    my_struct& operator= (const my_struct& s) noexcept {
        std::cout << __PRETTY_FUNCTION__ << '\n';
        m_v = s.m_v;
        return *this;
    }
    my_struct& operator= (my_struct&& s) noexcept {
        std::cout << __PRETTY_FUNCTION__ << '\n';
        m_v = std::move(s.m_v); // not need, but whatever
        s.m_v = -1;
        return *this;
    }
    int m_v;
};

int main() {
    my_class<my_struct> mc;

    std::cout << "===========================================\n";
    {
    std::cout << "-------------------------------------------\n";
    std::cout << "(1.1)\n";
    my_struct s{3};
    std::cout << s.m_v << '\n';
    mc.insert(s);
    std::cout << s.m_v << '\n';
    }

    {
    std::cout << "-------------------------------------------\n";
    std::cout << "(1.2)\n";
    const my_struct s{3};
    std::cout << s.m_v << '\n';
    mc.insert(s);
    std::cout << s.m_v << '\n';
    }

    std::cout << "===========================================\n";
    {
    std::cout << "-------------------------------------------\n";
    std::cout << "(2.1)\n";
    mc.insert(my_struct{5});
    }
    {
    std::cout << "-------------------------------------------\n";
    std::cout << "(2.2)\n";
    my_struct s{5};
    std::cout << s.m_v << '\n';
    mc.insert(std::move(s));
    std::cout << s.m_v << '\n';
    }

    std::cout << "===========================================\n";
    {
    std::cout << "-------------------------------------------\n";
    std::cout << "(3.1)\n";
    std::vector<my_struct> v(5);
    for (std::size_t i = 0; i < v.size(); ++i) { v[i] = my_struct(i); }

    for (const auto& s : v) { std::cout << s.m_v << ' '; } std::cout << '\n';
    mc.insert_vector(v);
    for (const auto& s : v) { std::cout << s.m_v << ' '; } std::cout << '\n';
    }
    {
    std::cout << "-------------------------------------------\n";
    std::cout << "(3.2)\n";
    const std::vector<my_struct> v = []() {
        std::vector<my_struct> v(5);
        for (std::size_t i = 0; i < v.size(); ++i) { v[i] = my_struct(i); }
        return v;
    }();

    for (const auto& s : v) { std::cout << s.m_v << ' '; } std::cout << '\n';
    mc.insert_vector(v);
    for (const auto& s : v) { std::cout << s.m_v << ' '; } std::cout << '\n';
    }

    std::cout << "===========================================\n";
    {
    std::cout << "-------------------------------------------\n";
    std::cout << "(4)\n";
    std::vector<my_struct> v(5);
    for (std::size_t i = 0; i < v.size(); ++i) { v[i] = my_struct(i); }

    for (const auto& s : v) { std::cout << s.m_v << ' '; } std::cout << '\n';
    mc.insert_vector(std::move(v));
    for (const auto& s : v) { std::cout << s.m_v << ' '; } std::cout << '\n';
    }
}

Solution

  • I think I found a way to solve this issue. I used the same "technique" as with the regular insert methods. The full implementation of my_class is this. Unfortunately, I could not make a single insert method.

    template <typename T>
    class my_class {
    private:
        T m_data;
    
    private:
        template <typename U>
        void do_some_more_work(U&& x) noexcept {
            std::cout << __PRETTY_FUNCTION__ << '\n';
            m_data = std::forward<U>(x);
        }
        template <typename U>
        void do_some_work(U&& x) noexcept {
            std::cout << __PRETTY_FUNCTION__ << '\n';
            do_some_more_work(std::forward<U>(x));
        }
    
    public:
        template <typename U>
        void insert(U&& x) noexcept {
            std::cout << __PRETTY_FUNCTION__ << '\n';
            do_some_work(std::forward<U>(x));
        }
    
        template <typename U>
        void insert_vector(U&& v) noexcept {
            using elem_t = typename std::remove_reference_t<U>::value_type;
    
            // this is used here for testing purposes.
            static_assert(std::is_same_v<elem_t, my_struct>);
    
            std::cout << __PRETTY_FUNCTION__ << '\n';
            for (std::size_t i = 0; i < v.size(); ++i) {
                if constexpr (std::is_same_v<std::vector<elem_t>, U>) {
                    do_some_work(std::forward<T>(v[i]));
                }
                else {
                    do_some_work(v[i]);
                }
            }
        }
        
    };