Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can template instances not be deduced in `std::reference_wrapper`s?

Suppose I have some object of type T, and I want to put it into a reference wrapper:

int a = 5, b = 7;

std::reference_wrapper<int> p(a), q(b);   // or "auto p = std::ref(a)"

Now I can readily say if (p < q), because the reference wrapper has a conversion to its wrapped type. All is happy, and I can process a collection of reference wrappers just like they were the original objects.

(As the question linked below shows, this can be a useful way to produce an alternate view of an existing collection, which can be rearranged at will without incurring the cost of a full copy, as well as maintaining update integrity with the original collection.)


However, with some classes this doesn't work:

std::string s1 = "hello", s2 = "world";

std::reference_wrapper<std::string> t1(s1), t2(s2);

return t1 < t2;  // ERROR

My workaround is to define a predicate as in this answer*; but my question is:

Why and when can operators be applied to reference wrappers and transparently use the operators of the wrapped types? Why does it fail for std::string? What has it got to do with the fact that std::string is a template instance?

*) Update: In the light of the answers, it seems that using std::less<T>() is a general solution.

like image 410
Kerrek SB Avatar asked Dec 14 '11 23:12

Kerrek SB


People also ask

Why use std:: reference_ wrapper?

std::reference_wrapper is useful in combination with templates. It wraps an object by storing a pointer to it, allowing for reassignment and copy while mimicking its usual semantics. It also instructs certain library templates to store references instead of objects.

What is reference_wrapper?

A reference_wrapper<Ty> is a copy constructible and copy assignable wrapper around a reference to an object or a function of type Ty , and holds a pointer that points to an object of that type. A reference_wrapper can be used to store references in standard containers, and to pass objects by reference to std::bind .


1 Answers

Edit: Moved my guesswork to the bottom, here comes the normative text why this won't work. TL;DR version:

No conversions allowed if the function parameter contains a deduced template parameter.


§14.8.3 [temp.over] p1

[...] When a call to that name is written (explicitly, or implicitly using the operator notation), template argument deduction (14.8.2) and checking of any explicit template arguments (14.3) are performed for each function template to find the template argument values (if any) that can be used with that function template to instantiate a function template specialization that can be invoked with the call arguments.

§14.8.2.1 [temp.deduct.call] p4

[...] [ Note: as specified in 14.8.1, implicit conversions will be performed on a function argument to convert it to the type of the corresponding function parameter if the parameter contains no template-parameters that participate in template argument deduction. [...] —end note ]

§14.8.1 [temp.arg.explicit] p6

Implicit conversions (Clause 4) will be performed on a function argument to convert it to the type of the corresponding function parameter if the parameter type contains no template-parameters that participate in template argument deduction. [ Note: Template parameters do not participate in template argument deduction if they are explicitly specified. [...] —end note ]

Since std::basic_string depends on deduced template parameters (CharT, Traits), no conversions are allowed.


This is kind of a chicken and egg problem. To deduce the template argument, it needs an actual instance of std::basic_string. To convert to the wrapped type, a conversion target is needed. That target has to be an actual type, which a class template is not. The compiler would have to test all possible instantiations of std::basic_string against the conversion operator or something like that, which is impossible.

Suppose the following minimal testcase:

#include <functional>

template<class T>
struct foo{
    int value;
};

template<class T>
bool operator<(foo<T> const& lhs, foo<T> const& rhs){
    return lhs.value < rhs.value;
}

// comment this out to get a deduction failure
bool operator<(foo<int> const& lhs, foo<int> const& rhs){
    return lhs.value < rhs.value;
}

int main(){
    foo<int> f1 = { 1 }, f2 = { 2 };
    auto ref1 = std::ref(f1), ref2 = std::ref(f2);
    ref1 < ref2;
}

If we don't provide the overload for an instantiation on int, the deduction fails. If we provide that overload, it's something the compiler can test against with the one allowed user-defined conversion (foo<int> const& being the conversion target). Since the conversion matches in this case, overload resolution succeeds and we got our function call.

like image 124
Xeo Avatar answered Sep 24 '22 08:09

Xeo