18

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.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • 5
    I suspect it has to do with the fact that the operator in question is a free template function. Since the inputs are currently reference_wrapper > and not some specialization of basic_string< CharT, CharTraits > the template function is not considered and not available to cause the implicit conversions. However, my standards-fu is pretty weak, and I'll leave it to those who are more familiar to answer. – Dave S Dec 14 '11 at 23:20
  • @DaveS: I think you're on the right track. An elaborate answer to that effect would be highly appreciated. – Kerrek SB Dec 14 '11 at 23:24

2 Answers2

9

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.

Xeo
  • 129,499
  • 52
  • 291
  • 397
  • An example or counterexample of where the deduction fails (and how that's related to `string` being a template instance unlike `int`) would be just as useful! :-) – Kerrek SB Dec 14 '11 at 23:27
  • @Kerrek: Added a small example, still looking for something to quote from the standard (I'm just not happy without one on these kinds of questions). – Xeo Dec 14 '11 at 23:32
  • 1
    @Kerrek: There, got the standard quotes. :) – Xeo Dec 15 '11 at 00:40
  • "[ 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 ]" interesting, but that is only informative text. (It is void if not backed-up by normative text.) – curiousguy Dec 15 '11 at 00:42
  • @curiousguy: It *is* backed up, it's the quote right below in my answer. – Xeo Dec 15 '11 at 00:43
  • "Implicit conversions (Clause 4) " **The question is not about Standard conversions, but about implicit conversions.** The quote is irrelevant. – curiousguy Dec 15 '11 at 01:20
  • @curiousguy: "[ Note: For user-defined types, user-defined conversions are considered as well; see 12.3. In general, an implicit conversion sequence (13.3.3.1) consists of a standard conversion sequence followed by a user-defined conversion followed by another standard conversion sequence. —end note ]" `§4 [conv] p4`. – Xeo Dec 15 '11 at 01:26
  • By your reading of the quoted standard clause, this clause is wrong. By my reading, this clause is just useless. – curiousguy Dec 15 '11 at 01:26
  • Maybe... any way, clause reads "blablabla if blobloblo" blobloblo does not apply, so blablabla does not either. – curiousguy Dec 15 '11 at 01:28
  • This clause is at the wrong place (many people will just not look into [temp.arg.**explicit**] for rules about calls without explicit template parameters), this clause is referencing the wrong section ((Clause 4) should be "13.3.3.1 Implicit conversion sequences"), this clause is confusing (we interpret it differently, even outside of the context of [temp.arg.explicit]) and **this clause is redundant** (if it is correct). What else? – curiousguy Dec 15 '11 at 01:38
  • @curi what text are you looking for? There was work done on this area in c++11. – Johannes Schaub - litb Dec 15 '11 at 08:58
  • Fwiw i agree that the rule under discussion is not responsible for the diagnostic – Johannes Schaub - litb Dec 15 '11 at 09:02
  • @JohannesSchaub-litb I am not looking for anything at the moment, thank you. – curiousguy Dec 16 '11 at 15:26
  • @Johannes: Why shouldn't it? What else could the rule talking about? – Xeo Dec 16 '11 at 15:42
6

std::reference_wrapper does not have an operator<, so the only way to do ref_wrapper<ref_wrapper is via the ref_wrapper member:

operator T& () const noexcept;

As you know, std::string is:

typedef basic_string<char> string;

The relevant declaration for string<string is:

template<class charT, class traits, class Allocator>
bool operator< (const basic_string<charT,traits,Allocator>& lhs, 
                const basic_string<charT,traits,Allocator>& rhs) noexcept;

For string<string this function declaration template is instantiated by matching string = basic_string<charT,traits,Allocator> which resolves to charT = char, etc.

Because std::reference_wrapper (or any of its (zero) bases classes) cannot match basic_string<charT,traits,Allocator>, the function declaration template cannot be instantiated into a function declaration, and cannot participate in overloading.

What matters here is that there is no non-template operator< (string, string) prototype.

Minimal code showing the problem

template <typename T>
class Parametrized {};

template <typename T>
void f (Parametrized<T>);

Parametrized<int> p_i;

class Convertible {
public:
    operator Parametrized<int> ();
};

Convertible c;

int main() {
    f (p_i); // deduce template parameter (T = int)
    f (c);   // error: cannot instantiate template
}

Gives:

In function 'int main()':
Line 18: error: no matching function for call to 'f(Convertible&)'

Standard citations

14.8.2.1 Deducing template arguments from a function call [temp.deduct.call]

Template argument deduction is done by comparing each function template parameter type (call it P) with the type of the corresponding argument of the call (call it A) as described below.

(...)

In general, the deduction process attempts to find template argument values that will make the deduced A identical to A (after the type A is transformed as described above). However, there are three cases that allow a difference:

  • If the original P is a reference type, the deduced A (i.e., the type referred to by the reference) can be more cv-qualified than the transformed A.

Note that this is the case with std::string()<std::string().

  • The transformed A can be another pointer or pointer to member type that can be converted to the deduced A via a qualification conversion (4.4).

See comment below.

  • If P is a class and P has the form simple-template-id, then the transformed A can be a derived class of the deduced A.

Comment

This implies that in this paragraph:

14.8.1 Explicit template argument specification [temp.arg.explicit]/6

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.

the if should not be taken as a if and only if, as it would directly contradict the text quoted previously.

Community
  • 1
  • 1
curiousguy
  • 8,038
  • 2
  • 40
  • 58