c++gccnamespaceslanguage-lawyerimplicit-conversion

C++ converting constructor behavior changed in gcc 12?


I am observing a weird behavior, which I cannot explain. The following code (a simplified version of the actual code) compiles correctly in gcc 11.4, and the argument "val" inside the read() method of MyStruct is implicitly converted to a Dummy using the converting constructor.

#include <iostream>

namespace secret_impl_space
{
  template <typename T>
  struct MyStruct
  {
    T val;
    MyStruct(T v): val(v) {}
    // the val below may be implicitly converted to Dummy if operator >> is missing from T
    virtual std::istream& read(std::istream& i) { i >> val; return i; }
  };

  struct Dummy { template<typename T> Dummy(const T& v){}; };

  inline std::istream& operator>>(std::istream& i, const Dummy& v)
  {
    std::cout << "Using Dummy" << std::endl; return i;
  }
}

struct A {int a;};

int main()
{
  A aa{1};
  secret_impl_space::MyStruct<A>* test (new secret_impl_space::MyStruct<A>(aa));
  return 0;
}

However, I found that newer gcc versions, starting from 12 on, give me the following compilation error (confirmed with godbolt):

no match for ‘operator>>’ (operand types are ‘std::istream’ {aka ‘std::basic_istream<char>’} and ‘A’)

The weirdest thing is that the code compiles correctly on any gcc version if I do one of the following two things:

  1. Get rid of the namespace "secret_impl_space"
  2. Remove the virtual specifier from the read() method.

Can someone explain this behavior? I am honestly puzzled.

Note: just to give the readers some context, in the original code MyStruct was the implementation part of a type-erasing container like boost::any - that is why it has a virtual >> method, to overload the one in the type-erased base interface. The whole idea behind defining the Dummy class was to allow using the type-erased container also for some types that do not have a >> operator - generating a Runtime warning instead of a compiler error. This is pretty terrible IMHO, but I did not write this, it was already around when I found the problem. All this machinery was 'hidden', for some reason (shame?), inside a namespace.


Solution

  • By all means this shoudn't have been working. By ADL rule (aka incorrectly Koenig rule) for a function call in template context where matching function is not declared yet, name lookup is deferred until instantiation.

    At point of instantiation, for a call with arguments std::basic_istream<char> and <global namespace>::A only namespace std and secret_impl_space should've been considered for look-up, considering undeclared converion or looking in global namespace (where A is defined) would be incorrect. The look-up is happening at point of instantiation. The error lists these types as a manner of reasoning.

    For virtual function of template

    point of instantiation is immediately following the point of instantiation of its enclosing class template specialization.

    Prior to C++11 this was implementation-defined. Otherwise inline member function of template have their point of instantiation at same location as their ODR-use (and may not happen at all for unused ones). The operator definition in posted code is within the "gap" between these two.

    The code can be fixed by moving declaration of Dummy and operator= above declaration of MyStruct. It can appear as a prototype:

    namespace secret_impl_space
    {
      struct Dummy { template<typename T> Dummy(const T& v){}; };
    
      std::istream& operator>>(std::istream& i, const Dummy& v);
    
      template <typename T>
      struct MyStruct
      {
        T val;
        MyStruct(T v): val(v) {}
       
        virtual std::istream& read(std::istream& i) { i >> val; return i; }
      };
      // at this point virtual MyStruct::read must be valid. 
      // If deleted or undeclared functions or types are ODR-used, 
      // the code of  MyStruct::read is ill-formed, IFNDR.
    
      inline std::istream& operator>>(std::istream& i, const Dummy& v)
      {
        std::cout << "Using Dummy" << std::endl; return i;
      }
    }
    
    struct A {int a;};
    
    int main()
    {
      A aa{1};
      // at this point a non-virtual member of MyStruct will be instantiated
      secret_impl_space::MyStruct<A>* test (new secret_impl_space::MyStruct<A>(aa));
      return 0;
    }
    

    The change of behavior possibly result of a fix for some flaws in ADL implementation.

    Original code is also rejected by clang. Some versions of MSVC may accept it due to non-compliant beahviour of look-up starting in global namespace.