Почему экземпляры шаблона не могут быть выведены в `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, поскольку это прямо противоречило бы приведенному ранее тексту.

Другие вопросы по тегам