Is it possible to write a class that can store lvalue references or objects exlusively?

141 views Asked by At

I'm sick of passing all my objects by value and then moving, or overloading on lvalues and rvalues. I'm trying to write a class that can either store an lvalue reference or a normal object if an rvalue is passed into the constructor.

Here is the skeleton:

template<typename T>
class ref {
private:
    T* m_ptr = nullptr;
    std::optional<T> m_obj;
    bool is_lvalue = true;
public:
    ref(T& lvalue_ref) : m_ptr{ &lvalue_ref } {}
    ref(T&& rvalue_ref) : m_obj{ std::move(rvalue_ref) }, is_lvalue{ false } {}

    T& get() {
        return (is_lvalue) ? *m_ptr : *m_obj;
    }
};

I want to use this class with functions like void foo(ref<bar> r) {}, so any function taking ref<T> does not make any copies. Unfortunately, you have to check every time if a reference/object is stored whenever you get the value, and std::optional default constructs its object and we pay for that even if we are storing an lvalue. Is it possible to create a class that stores either an lvalue reference, or an object, and still be able to have a function taking ref<T> be able to bind both lvalues and rvalues of T to it?

1

There are 1 answers

0
Enlico On

I'll first quote my comment, so everybody can have a full view of my... view.

I agreen with @JaMiT this is an XY problem. Indeed, the question features the skeleton of a possible solution to a not well defined problem: you write "I'm sick of passing all my objects by value and then moving, or overloading on lvalues and rvalues". How? Show an example of code that led you to think you need a solution to a problem. Are the context where you overload on rvalue ref and lvalue ref generic contexts? Or are you referring to functions taking concrete values? None of these facets is explained in the Q.

But I still think some guessing can be attempted.

From

I'm sick of passing all my objects by value and then moving, or overloading on lvalues and rvalues.

and

want to use this class with functions like void foo(ref<bar> r) {}, so any function taking ref<T> does not make any copies.

I assume you have many functions taking args by value, and/or many functions overloaded for & and &&, in either case the type(s) of the arg(s) being concrete (otherwise, if those functions were templated, I'd have expected at least a mention of a forwarding reference in the body of your question).

To avoid the copies, I think you'd go (or you are going already) for a solution like this, which is repeated for several functions, where you duplicate not just the interfaces in a header,

// foo.hpp
#pragma once
#include "Bar.hpp"
void foo(Bar&);
void foo(Bar&&);

but also the business logic in the implementation files:

// foo.cpp
#include "Bar.hpp"
void foo(Bar& x) {
    business_logic(x);
}
void foo(Bar&&) {
    business_logic(std::move(x));
}

a typical client code looking like this,

// main.cpp
#include "foo.hpp"
#include "Bar.hpp"
int main()
{
    foo(Bar{3});
    Bar x{3};
    foo(x);
}

If that's the case, one solution could be to just template the functions and impletement them in the cpp file, together with the two explicit instantiations you need:

// foo.hpp
#pragma once
template<typename T>
void foo(T&&);
// foo.cpp
#include "Bar.hpp"
template<typename T>
void foo(T&& t) {
    business_logic(std::forward<T>(t));
}
template void foo(Bar&);
template void foo(Bar&&);

Notice that my toy example has the same length in both cases, but there are some differences between the two. In my proposed solution,

  • there is only one implementation shared for rvalues and lvalues, so you are not duplicating business logic;
  • you can just std::forward<T> the argument to other functions, whereas in the "manual" overload case you have to write std::move in one overload and nothing in the other;
  • a test could have the benefit of exercising foo with a simpler type than Bar by just instantiating foo for a BarForTests mock type.