Почему экземпляры шаблонов не могут быть выведены в `std:: reference_wrapper`?

Предположим, что у меня есть некоторый объект типа T, и я хочу поместить его в ссылочную оболочку:

int a = 5, b = 7;

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

Теперь я могу легко сказать if (p < q), потому что эталонная оболочка имеет преобразование в свой завернутый тип. Все счастливы, и я могу обработать набор ссылочных оберток, как и исходные объекты.

(Как показывает приведенный ниже вопрос, приведенный ниже, это может быть полезным способом для создания альтернативного представления существующей коллекции, которая может быть переупорядочена по желанию без каких-либо затрат на полную копию, а также с сохранением целостности обновления с исходной коллекцией.)


Однако, с некоторыми классами это не работает:

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

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

return t1 < t2;  // ERROR

Мое обходное решение - определить предикат , как в этот ответ *; но мой вопрос:

Почему и когда операторы могут применяться к ссылочным оберткам и прозрачно использовать операторов обернутых типов? Почему это не работает для std::string? Что это связано с тем, что std::string является экземпляром шаблона?

*) Обновление. В свете ответов кажется, что использование std::less<T>() является общим решением.

Ответы

Ответ 1

Изменить: Переместил мои догадки на дно, здесь идет нормативный текст, почему это не сработает. Версия TL: DR:

Разрешения конверсий не допускаются, если параметр функции содержит параметр выведенного шаблона.


§14.8.3 [temp.over] p1

[...] Когда вызов этого имени записывается (явно или неявно с использованием оператора нотация), вывод аргумента шаблона (14.8.2) и проверка любых явных аргументов шаблона (14.3) для каждого шаблона функции, чтобы найти значения аргумента шаблона (если они есть), которые можно использовать с этим шаблоном функции для создания экземпляра шаблона функции специализацию, которая может быть вызвана с помощью аргументов вызова.

§14.8.2.1 [temp.deduct.call] p4

[...] [Примечание: как указано в 14.8.1, неявные преобразования будут выполняться на аргументе функции, чтобы преобразовать его в тип соответствующего параметра функции , если Параметр не содержит шаблонных параметров, которые участвуют в выводе аргумента шаблона. [...] -end note]

§14.8.1 [temp.arg.explicit] p6

Неявные преобразования (раздел 4) будут выполняться на аргументе функции, чтобы преобразовать его в тип соответствующего параметра функции, если тип параметра не содержит шаблонных параметров, которые участвуют в выводе аргумента шаблона. [Примечание. Параметры шаблона не участвуют в выводе аргумента шаблона, если они явно указаны. [...] -end note]

Так как std::basic_string зависит от выведенных параметров шаблона (CharT, Traits), конверсии не допускаются.


Это своего рода проблема с курицей и яйцом. Чтобы вывести аргумент шаблона, ему нужен фактический экземпляр std::basic_string. Чтобы преобразовать в завернутый тип, необходима цель преобразования. Эта цель должна быть фактическим типом, который не является шаблоном класса. Компилятор должен будет проверить все возможные экземпляры std::basic_string для оператора преобразования или что-то в этом роде, что невозможно.

Предположим, что следующий минимальный тест:

#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;
}

Если мы не обеспечиваем перегрузку для экземпляра на int, вывод не выполняется. Если мы предоставим эту перегрузку, это то, что компилятор может протестировать против одного разрешенного пользователем преобразования (foo<int> const& является целью преобразования). Поскольку преобразование соответствует в этом случае, разрешение перегрузки преуспевает, и мы получили вызов нашей функции.

Ответ 2

std::reference_wrapper не имеет operator<, поэтому единственный способ сделать ref_wrapper<ref_wrapper - через член ref_wrapper:

operator T& () const noexcept;

Как вы знаете, std::string:

typedef basic_string<char> string;

Соответствующая декларация для string<string:

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

Для string<string этот шаблон объявления этой функции создается путем сопоставления string= basic_string<charT,traits,Allocator>, который разрешает charT= char и т.д.

Поскольку std::reference_wrapper (или любой из его (нулевых) классов баз) не может соответствовать basic_string<charT,traits,Allocator>, шаблон объявления функции не может быть создан в объявлении функции и не может участвовать в перегрузке.

Здесь важно, что существует прототип no.

Минимальный код, показывающий проблему

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
}

Дает:

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

Стандартные ссылки

14.8.2.1 Вывод аргументов шаблона из вызова функции [temp.deduct.call]

Вывод аргумента шаблона производится путем сравнения каждого параметра параметра шаблона функции (вызывайте его P) с типом соответствующего аргумента вызова (назовите его A), как описано ниже.

(...)

В целом процесс дедукции пытается найти значения аргументов шаблона, которые сделают вывод A идентичным A (после преобразования типа A, как описано выше). Однако есть три случая, которые позволяют разницу:

  • Если исходный P является ссылочным типом, выводимый A (т.е. тип, на который ссылается ссылка) может быть больше CV, чем преобразованный A.

Обратите внимание, что это имеет место с std::string()<std::string().

  • Преобразованный A может быть другим указателем или указателем на тип члена, который может быть преобразован в выведенный A через квалификационное преобразование (4.4).

См. комментарий ниже.

  • Если P является классом, а P имеет форму simple-template-id, преобразованный A может быть производным классом выведенного A.

Комментарий

Это означает, что в этом параграфе:

14.8.1 Явная спецификация аргументов шаблона [temp.arg.explicit]/6

Неявные преобразования (раздел 4) будут выполняться на аргументе функции, чтобы преобразовать его в тип соответствующего параметра функции , если тип параметра не содержит шаблонных параметров, которые участвуют в выводе аргумента шаблона.

, если не следует воспринимать как , если и только если, поскольку это прямо противоречит цитированному ранее тексту.