Следующий код нормально компилируется с GCC 13 и более ранними версиями, но GCC 14.1 выдает ошибку «неоднозначной перегрузки». Это проблема компилятора или кода, и, более прагматично, могу ли я заставить компилятор отдавать предпочтение шаблону, не являющемуся членом, внеся изменения в пространство имен ns
(оставшись в среде C++11)?
#include <iostream>
#include <sstream>
#include <string>
//=======================================================================
namespace ns {
struct B
{
std::ostringstream os_;
~B()
{
std::cerr << os_.str() << '\n';
}
template<typename T>
B& operator<<(T const& v)
{
this->f(v);
this->os_ << ' ';
return *this;
}
private:
void f(int v) { os_ << v; }
void f(std::string const& v) { os_ << "\"" << v << '\"'; }
};
struct D : public B
{};
}
//==============================================================
namespace nsa {
struct A
{
int i;
std::string s;
};
template<typename S>
S& operator<<(S&& s, A const & a)
{
s << "S<<A" << a.i << a.s;
return s;
}
}
//==============================================================
int main()
{
ns::D() << "XX" << nsa::A{1, "a"};
}
GCC 13 успешно компилирует его, и выходные данные программы
"XX" "S<<A" 1 "a"
Вывод компилятора GCC 14:
In function 'int main()':
<source>:50:19: error: ambiguous overload for 'operator<<' (operand types are 'ns::B' and 'nsa::A')
50 | ns::D() << "XX" << nsa::A{1, "a"};
| ~~~~~~~~~~~ ^~ ~~~~~~~~~
| | |
| ns::B nsa::A
<source>:17:8: note: candidate: 'ns::B& ns::B::operator<<(const T&) [with T = nsa::A]'
17 | B& operator<<(T const& v)
| ^~~~~~~~
<source>:41:6: note: candidate: 'S& nsa::operator<<(S&&, const A&) [with S = ns::B&]'
41 | S& operator<<(S&& s, A const & a)
| ^~~~~~~~
Я думал, что отсутствие других B::f()
приведет к сбою замены и исключению шаблона-членаoperator<<() из набора перегрузки.
Несколько версий clang считают, что это неоднозначная перегрузка.
Кажется, MSVC пытается преобразовать A в int
или в string
, как будто он вообще не видит шаблон, не являющийся членом, и выводит что-то вроде
<source>(22): error C2664: 'void ns::B::f(const std::string &)': cannot convert argument 1 from 'const T' to 'int'
with
[
T=nsa::A
]
<source>(22): note: No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called
<source>(53): note: see reference to function template instantiation 'ns::B &ns::B::operator <<<nsa::A>(const T &)' being compiled
with
[
T=nsa::A
]
Ответ на этот вопрос, похоже, объясняет двусмысленность, хотя там нет вызовов несуществующих функций, поэтому я бы не ожидал, что SFINAE сработает в этом примере.
Добавление enable_if
к шаблону члена, по-видимому, работает не очень хорошо, поскольку могут существовать типы, конвертируемые в int
, для которых может потребоваться опционально определить шаблон, не являющийся членом.
«Могу ли я заставить компилятор отдавать предпочтение шаблону, не являющемуся членом, внеся изменения в пространство имен ns...» Да, один из способов устранить двусмысленность — удалить константу низкого уровня из параметра ns::B::operator<<
. Демо
Примечание: существует еще один способ устранения двусмысленности (путем преобразования T const& v
в T&& v
), но при этом будет использоваться шаблон участника, а это не то, что вам нужно. Итак, в моем ответе: если вы удалите изменение T const& v
на T & v
, вы получите желаемое поведение.
«Я думал, что отсутствие других B::f() приведет к ошибке замены»: SFINAE учитывает только объявление функций, которые рассматриваются для разрешения перегрузки. Он ничего не решает на основе тела функции. В C++ нет ничего, что позволяло бы компилятору проверять корректность тела функции.
могу ли я заставить компилятор отдавать предпочтение шаблону, не являющемуся членом, внеся изменения в пространство имен
ns
Да, вы можете устранить неоднозначность, удалив константу низкого уровня из параметра ns::B::operator<<
, как показано ниже. Благодаря этому шаблон для неучастников будет лучше подходить, чем версия для участников operator<<
.
По сути, считается, что функции-члены имеют неявный объектный параметр для целей разрешения перегрузки. Это приводит к тому, что обе версии в вашем примере будут иметь одинаковый ранг (никто не лучше/хуже другого). То же самое объясняется в Разрешение перегрузки шаблонного оператора, функция-член и функция, не являющаяся членом
namespace ns {
struct B
{
//other code as before
template<typename T>
//-------------vv------>removed low-level const from here
B& operator<<(T & v)
{
this->f(v);
this->os_ << ' ';
return *this;
}
//other code as before
}
Обратите внимание: если сделать его неконстантным, это означает, что изменения можно вносить в v
внутри функции. Кроме того, двусмысленность возвращается, если пользователь пытается использовать оператор с уточнением const T
.
@TedLyngmo Добавил это в конце ответа. Кстати, я изменил «будет» на «может быть» в вашем комментарии, добавляя его в конце ответа. Поскольку в данном примере они не собираются менять элемент данных.
Удаление const
кажется неправильным, потому что это запретит код типа ns::D() << 2;
или nsa::A a{1, "a"}; ns::D() << a.getInt();
, если предположить, что функция int getInt() const
добавлена в A
И nsa::A a{1, "a"}; ns::D() << a;
не компилируется с MSVC и генерирует массу предупреждений в GCC.
Это проблема компилятора или кода?
Проблема с кодом.
Диагностика gcc 14 довольно ясна С
ns::D() << "XX" << nsa::A{1, "a"};
// ^^
У нас есть «ns::B& << nsa::A
» (ns::D
«теряется» с помощью ns::D() << "XX"
, который возвращает ns::B
)
и у нас есть 2 одинаковые перегрузки (точное совпадение)
ns::B::operator<< (const T&)
с T == nsa::A
nsa::operator<< (T&&, const A&)
с T == ns::B&
ни один из них не является более специализированным, чем другой.
SFINAE не бывает на теле.
Вы можете применить SFINAE, который удаляет ns::B::operator<<
из жизнеспособной функции, устраняя двусмысленность в вашем случае:
template<typename T>
auto operator<<(T const& v)
-> decltype(this->f(v), *this)
{
this->f(v);
this->os_ << ' ';
return *this;
}
Если вы правильно поняли, вы помещаете в объявление выражение, вызывающее ошибку замены, но отбрасываете его тип, если замена успешна. Гениально! К сожалению, я ищу решение C++11.
К счастью, это C++11 (как вы можете видеть в demo.
Да, в самом деле. Это работает довольно хорошо, за исключением случая, когда есть enum
с нечленом operator<<()
, поскольку он конвертируется в int
. namespace nsa { enum E { E0, E1 }; template <typename S> operator<<(S&& s, E e) { switch(e) { case E0: s << "E0"; break; case E1: s << "E1"; break; default: s << "EX"; break; } return s; } } ... ns::D() << "Enum" << nsa::E_0;
Он работает как в MSVC, так и в GCC до 14, но в GCC 14 все еще неоднозначно. Есть предложения, как с этим справиться?
SFINAE также на is_enum
: Демо
Мне пришел в голову такой подход, но он не работает, если только у некоторых enums
есть собственный шаблон operator<<()
, а другие довольны просто конвертацией в int
.
Я считаю, что мне удалось достичь того, что мне нужно, поместив код шаблона члена в шаблон частной функции, заменив шаблон члена B::operator<<()
кучей перегрузок членов, не являющихся шаблонами, и перенаправив вызов в частный шаблон (godbolt) . Оглядываясь назад, это кажется очевидным. хотя и утомительный, но способ избежать создания дубликатов кандидатов для разрешения перегрузки.
Ошибка довольно проста и вызвана той же причиной, что и объяснение в вашем связанном вопросе . Но тут хочется вызвать неучастнику
template<typename S> S& operator<<(S&& s, A const & a)