Неоднозначное разрешение шаблонов функций-членов и функций, не являющихся членами C++, в GCC 14, но не в предыдущих версиях GCC

Следующий код нормально компилируется с 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, для которых может потребоваться опционально определить шаблон, не являющийся членом.

Ошибка довольно проста и вызвана той же причиной, что и объяснение в вашем связанном вопросе . Но тут хочется вызвать неучастнику template<typename S> S& operator<<(S&& s, A const & a)

user12002570 06.07.2024 16:35

«Могу ли я заставить компилятор отдавать предпочтение шаблону, не являющемуся членом, внеся изменения в пространство имен ns...» Да, один из способов устранить двусмысленность — удалить константу низкого уровня из параметра ns::B::operator<<. Демо

user12002570 06.07.2024 16:42

Примечание: существует еще один способ устранения двусмысленности (путем преобразования T const& v в T&& v), но при этом будет использоваться шаблон участника, а это не то, что вам нужно. Итак, в моем ответе: если вы удалите изменение T const& v на T & v, вы получите желаемое поведение.

user12002570 06.07.2024 16:49

«Я думал, что отсутствие других B::f() приведет к ошибке замены»: SFINAE учитывает только объявление функций, которые рассматриваются для разрешения перегрузки. Он ничего не решает на основе тела функции. В C++ нет ничего, что позволяло бы компилятору проверять корректность тела функции.

user17732522 06.07.2024 17:09
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
4
97
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

могу ли я заставить компилятор отдавать предпочтение шаблону, не являющемуся членом, внеся изменения в пространство имен 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 Добавил это в конце ответа. Кстати, я изменил «будет» на «может быть» в вашем комментарии, добавляя его в конце ответа. Поскольку в данном примере они не собираются менять элемент данных.

user12002570 06.07.2024 16:54

Удаление const кажется неправильным, потому что это запретит код типа ns::D() << 2; или nsa::A a{1, "a"}; ns::D() << a.getInt();, если предположить, что функция int getInt() const добавлена ​​в A

akryukov 07.07.2024 00:33

И nsa::A a{1, "a"}; ns::D() << a; не компилируется с MSVC и генерирует массу предупреждений в GCC.

akryukov 07.07.2024 00:40
Ответ принят как подходящий

Это проблема компилятора или кода?

Проблема с кодом.

Диагностика 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.

akryukov 07.07.2024 00:58

К счастью, это C++11 (как вы можете видеть в demo.

Jarod42 07.07.2024 01:13

Да, в самом деле. Это работает довольно хорошо, за исключением случая, когда есть 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 все еще неоднозначно. Есть предложения, как с этим справиться?

akryukov 08.07.2024 01:49

SFINAE также на is_enum: Демо

Jarod42 08.07.2024 03:01

Мне пришел в голову такой подход, но он не работает, если только у некоторых enums есть собственный шаблон operator<<(), а другие довольны просто конвертацией в int.

akryukov 08.07.2024 15:05

Я считаю, что мне удалось достичь того, что мне нужно, поместив код шаблона члена в шаблон частной функции, заменив шаблон члена B::operator<<() кучей перегрузок членов, не являющихся шаблонами, и перенаправив вызов в частный шаблон (godbolt) . Оглядываясь назад, это кажется очевидным. хотя и утомительный, но способ избежать создания дубликатов кандидатов для разрешения перегрузки.

akryukov 09.07.2024 01:42

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