Почему экземпляры шаблона не могут быть выведены в `std::reference_wrapper`s?
Предположим, у меня есть какой-то объект типа 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>()
это общее решение.
2 ответа
Изменить: переместил мои догадки на дно, здесь идет нормативный текст, почему это не будет работать. TL;DR версия:
Преобразования не допускаются, если параметр функции содержит выведенный параметр шаблона.
§14.8.3 [temp.over] p1
[...] Когда написано обращение к этому имени (явно или неявно с использованием нотации оператора), для каждого шаблона функции, для которого выполняется поиск, выполняется вывод аргумента шаблона (14.8.2) и проверка любых явных аргументов шаблона (14.3). значения аргументов шаблона (если они есть), которые можно использовать с этим шаблоном функции для создания экземпляра специализации шаблона функции, которая может быть вызвана с аргументами вызова.
§14.8.2.1 [temp.deduct.call] p4
[...] [ Примечание: как указано в 14.8.1, неявные преобразования будут выполняться для аргумента функции, чтобы преобразовать его в тип соответствующего параметра функции, если параметр не содержит параметров шаблона, которые участвуют в выводе аргумента шаблона. [...] - конец примечания ]
§14.8.1 [temp.arg.explicit] p6
Неявное преобразование (раздел 4) будет выполнено для аргумента функции, чтобы преобразовать его в тип соответствующего параметра функции, если тип параметра не содержит параметров шаблона, которые участвуют в выводе аргумента шаблона. [ Примечание: параметры шаблона не участвуют в выводе аргументов шаблона, если они указаны явно. [...] - конец примечания ]
поскольку 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&
быть целью конверсии). Поскольку в этом случае преобразование совпадает, разрешение перегрузки завершается успешно, и мы получили вызов функции.
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>
шаблон объявления функции не может быть создан в объявлении функции и не может участвовать в перегрузке.
Здесь важно то, что нет не шаблонов operator< (string, string)
прототип.
Минимальный код, показывающий проблему
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
(т. е. тип, на который ссылается ссылка) может быть более квалифицированным, чем преобразованныйA
,
Обратите внимание, что это в случае с std::string()<std::string()
,
- Преобразованный
A
может быть другим указателем или указателем на тип члена, который может быть преобразован в выведенныйA
через преобразование квалификации (4.4).
Смотрите комментарий ниже.
- Если
P
это класс иP
имеет вид simple-template-id, затем преобразованныйA
может быть производным классом выведенногоA
,
Комментарий
Это подразумевает, что в этом пункте:
14.8.1 Явная спецификация аргумента шаблона [temp.arg.explicit]/ 6
Неявное преобразование (раздел 4) будет выполнено для аргумента функции, чтобы преобразовать его в тип соответствующего параметра функции, если тип параметра не содержит параметров шаблона, которые участвуют в выводе аргумента шаблона.
if не следует воспринимать как if и only if, поскольку это прямо противоречило бы приведенному ранее тексту.