Martin Drozdik
Martin Drozdik

Reputation: 13313

How to correctly use std::reference_wrappers

I am trying to understand std::reference_wrapper.

The following code shows that the reference wrapper does not behave exactly like a reference.

#include <iostream>
#include <vector>
#include <functional>

int main()
{
    std::vector<int> numbers = {1, 3, 0, -8, 5, 3, 1};

    auto referenceWrapper = std::ref(numbers);
    std::vector<int>& reference = numbers;

    std::cout << reference[3]              << std::endl;
    std::cout << referenceWrapper.get()[3] << std::endl; 
              // I need to use get ^
              // otherwise does not compile.
    return 0;
}

If I understand it correctly, the implicit conversion does not apply to calling member functions. Is this an inherent limitation? Do I need to use the std::reference_wrapper::get so often?

Another case is this:

#include <iostream>
#include <functional>

int main()
{
    int a = 3;
    int b = 4;
    auto refa = std::ref(a);
    auto refb = std::ref(b);
    if (refa < refb)
        std::cout << "success" << std::endl;

    return 0;
}

This works fine, but when I add this above the main definition:

template <typename T>
bool operator < (T left, T right)
{
    return left.someMember();
}

The compiler tries to instantiate the template and forgets about implicit conversion and the built in operator.

Is this behavior inherent or am I misunderstanding something crucial about the std::reference_wrapper?

Upvotes: 57

Views: 39432

Answers (3)

Bolpat
Bolpat

Reputation: 1695

About the second part of the question, why refa < refb works as long as there is no template operator< defined, it’s how C++’s function overloading works. AFAICT, it has nothing to do with the fact that it’s an operator.

The first thing you need to know is that std::reference_wrapper<T> does not implement comparison operators before C++26. (In C++26, they’re implemented in the obvious way.) This means you get yourself in the messy space of overload resolution and template parameter substitution.

As it seems that the included headers declare no operator< function and no operator< function template with a successful substitution for your parameters (the std::reference_wrapper<int> objects).

Before you have your template<typename T> operator<(T, T) declared, overload resolution finds that it can call one operator<, namely operator<(int, int) if it does the user-defined implicit conversion on std::reference_wrapper<int>.

With your template<typename T> operator<(T, T) present, your code fails to compile. This is because Substitution Failure Is Not An Error (SFINAE for short). Substitution deals with inferring function template parameters based on the arguments passed to the function. Notably, substitution failure only cares about whether the signature is well-formed. If the function signature is fine, but something in the function body (its definition) goes wrong, it is not a substitution failure. You could use so-called SFINAE-friendly constructs such as std::enable_if to make your operator< function template fail substitution for a T that has no someMember(). That’s quite a bit of work, though.

Clearly, your function template operator< is being used. Why? Because overload resolution first generates a list of all candidates and then chooses the best among them. While the built-in operator<(int, int) still is a candidate (via user-defined implicit conversion), the function operator< <std::reference_wrapper<int>>(std::reference_wrapper<int>, std::reference_wrapper<int>) synthesized from your function template is a new competitor. For argument’s sake, let’s assume there aren’t any other candidates. (For reference, there are, such as operator<(long, long).) Those candidates are then ranked by how well they match the arguments in terms of how well the arguments convert to the parameter types. Those rules are complicated, but as a shortcut, an exact match is best and requiring user-defined conversions push a candidate down the ranks. For that reason, the synthesized function from your function template is a (much) better match than the built-in operator<(int, int).

Now, there are a tie-breaker rules that make non-template functions higher in the ranking than instances of a function template and rank-order template instances by degree of specialization:

  1. or, if not that, F1 is a non-template function while F2 is a template specialization
  2. or, if not that, F1 and F2 are both template specializations and F1 is more specialized according to the partial ordering rules for template specializations

Those tie-breakers are applied after considering how well arguments implicitly convert to parameter types.


For answering what you could do about the problem, I’ll assume you can’t change the template<typename T> bool operator<(T,T), otherwise that is the easiest solution by far.

If you just want to write refa < refb (without .get()), you could define operator<(std::reference_wrapper<int>, std::reference_wrapper<int>). As long as you don’t do that in the std namespace, that’s okay. A lot of people dislike that for good reasons, but it might just get the job done. You can even use template<typename T> bool operator<(std::reference_wrapper<T>, std::reference_wrapper<T>) as that one is more specialized than the one that fails. If you want to use the std::reference_wrapper<int> as a std::map key, probably the best solution is to write a class reference_wrapper_less:

template<typename T>
struct reference_wrapper_less
{
    bool operator(std::reference_wrapper<T> l, std::reference_wrapper<T> r)
    {
        return l.get() < r.get();
    }
};

Then, you use maps of the form std::map<std::reference_wrapper<int>, Value, reference_wrapper_less>.

Upvotes: 0

Bolpat
Bolpat

Reputation: 1695

The call to get seems to be necessary because std::reference_wrapper does not conditionally implement operator[]. It does so for operator() (see here). Confronted with a std::reference_wrapper<std::vector<int>> that uses operator[], for some reason, C++ does not attempt conversions like the implicit conversion of std::reference_wrapper to a reference. I’m not entirely sure why this is the case. I checked Clang and GCC (latest versions: v16.0.0 and v13.0.0) and they both reject it for C++11 through C++20 as well as C++1b.

Upvotes: 2

Cassio Neri
Cassio Neri

Reputation: 20523

Class std::reference_wrapper<T> implements an implicit converting operator to T&:

operator T& () const noexcept;

and a more explicit getter:

T& get() const noexcept;

The implicit operator is called when a T (or T&) is required. For instance

void f(some_type x);
// ...
std::reference_wrapper<some_type> x;
some_type y = x; // the implicit operator is called
f(x);            // the implicit operator is called and the result goes to f.

However, sometimes a T is not necessarily expected and, in this case, you must use get. This happens, mostly, in automatic type deduction contexts. For instance,

template <typename U>
g(U x);
// ...
std::reference_wrapper<some_type> x;
auto y = x; // the type of y is std::reference_wrapper<some_type>
g(x);       // U = std::reference_wrapper<some_type>

To get some_type instead of std::reference_wrapper<some_type> above you should do

auto y = x.get(); // the type of y is some_type
g(x.get());       // U = some_type

Alternativelly the last line above could be replaced by g<some_type>(x);. However, for templatized operators (e.g. ostream::operator <<()) I believe you can't explicit the type.

Upvotes: 62

Related Questions