Handling lvalue and rvalue references uniformly

Viewed 177

I have two related questions, both related to how to handle lvalue and rvalue references uniformly.

Virtual function case:

struct Foo {
    Object m_object;

    virtual void bar(SomeRefTypeOfObject object);
};

Basically, what I want to achieve is to have a SomeRefTypeOfObject, which is able to store both lvalue and rvalue reference to Object. bar is a larger function, and it will use one m_object = object; statement store the value of object (a copy or move operation, depending on the type of the stored reference). The reason is that I want to avoid having two bar functions (for each reference type). Is there anything in the standard library which can do this conveniently and efficiently, or do I have to roll my own solution for this? If I have to roll my own solution, how would a good implementation look like?

Template function case:

struct Foo {
    Object m_object;

    template <...>
    void bar(... object);
};

I'd like to have a bar template, which can be called with any kind of Object or its derived classes/other objects which can be converted to Object (like if bar were two overloaded functions with const Object & and Object && parameters), but instantiated with only const Object & and Object &&. So, I don't want to have bar instantiated for each derived type. What is the most clear way to do that? I suppose I'll need some form of SFINAE here.

Note: m_object = object; assignment can happen in a function which is called by bar. So the solution passing object by value is not optimal, as unnecessary copies/moves will be made (there can be types for which move is not that cheap as passing down a reference). Furthermore, the assignment can happen in a conditional way, so passing object by value just in the leaf function not a solution (because it can be runtime dependent, which function does the actual assignment).

3 Answers

The type erasure (or maybe reference erasure could be better term here) sketch of the approach:

#include <iostream>
#include <type_traits>
#include <utility>

struct Object { 
    Object() {
        std::cout << "Object()" << std::endl;
    }
    Object(const Object &) {
        std::cout << "Object(const Object &)" << std::endl;
    }
    Object(Object &&) {
        std::cout << "Object(Object &&)" << std::endl;
    }
    Object &operator =(const Object &) {
        std::cout << "Object &operator =(const Object &)" << std::endl;
        return *this;
    }
    Object &operator =(Object &&) {
        std::cout << "Object &operator =(Object &&)" << std::endl;
        return *this;
    }
};

template <class T>
struct RLReferenceWrapper {
    bool is_rvalue;
    T *pnull;
    const T& cref;
    T&& rref;
    RLReferenceWrapper(T&& rref): is_rvalue(true), pnull(nullptr), cref(*pnull), rref(std::move(rref)) { }
    RLReferenceWrapper(const T& cref): is_rvalue(false), pnull(nullptr), cref(cref), rref(std::move(*pnull)) { }

    template <class L>
    void Visit(L l) {
        if (is_rvalue) {
            l(std::move(rref));
        } else {
            l(cref);
        }
    }
};

struct Foo {
    Object m_object;

    virtual void bar(RLReferenceWrapper<Object> wrapper) {
        wrapper.Visit([this](auto&& ref){ 
            m_object = std::forward<std::remove_reference_t<decltype(ref)>>(ref);
        });
    }
};

int main() {
    Foo f;
    f.bar(Object{});
    Object o2;
    f.bar(o2);
}

Output:

Object()
Object()
Object &operator =(Object &&)
Object()
Object &operator =(const Object &)

[live demo]

an alternative solution might be ( not tested ):

template <class T> struct movable{};
template <class T> struct movable<T const&>
{
  movable( movable const& ) = delete;
  movable( T const& r ): ref_{ const_cast<T&>(r) }, movable_{false} {}
  movable( T&& r ): ref_{r}, movable_{true} {}

  auto get() const -> T const& { return ref_; }
  auto move() -> T&& { if( !movable_ ) throw std::runtime_error{"bad move!"}; movable_ = false; return std::move(ref_); }

  template <class V>
  void assign_or_move_to( V& v ) { if( movable_ ) v = move(); else v = get(); }

private:
  T&   ref_;
  bool movable_;
};

struct Foo {
  Object m_object;

  virtual void bar( movable<Object const&> object_ref )
  {
    auto& object = object_ref.get();

    // ...

    object_ref.assign_or_move_to( m_object );

    // or

    if( something ) m_object = object_ref.move();
  }
};

that said, I doubt it's worth it, you should still explain us why you refuse to write two bar overloads to begin with ...

How about

struct Foo {
    Object m_object;

    void bar(Object& o) { bar_impl(o); }
    void bar(Object&& o) { bar_impl(std::move(o)); }

    template <typename T>
    void bar_impl(T&& object) { 
        // snip ...
        m_object = std::forward<T>(object);
    }
};

I think this:

  1. Only instantiates bar for Object& and Object&&
  2. Only has one instance of the long function
  3. Allows template to pick copy-or-move depending on what is passed

It doesn't have a wrapper object or anything, but I think that's OK b/c T&& is not an rvalue as such, it's a universal reference as explained here: https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers (thanks Scott!)

Related