使用模板化重载的编译器差异

问题描述

我有一个非常具体的情况,我将一堆数据提供给一个类似 hasher 的类。特别是,我使用的一种数据类型有一个成员,其类型取决于超类型的类型参数。长话短说,这里有一段代码说明了这种行为:

#include <assert.h>
#include <iostream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>

// Some dummy priority structs to select overloads
struct priority0 { };
struct priority1 : priority0 { };

// This is the hasher-like function
struct Catcher
{
    // Ideally we Feed everything to this object through here
    template <typename T> Catcher& operator<<(const T& v)
    {
        add(v,priority1{}); // always attempt to call the highest-priority overload
        return *this;
    }
    
    // For floating-point data types
    template <typename T> auto add(const T& v,priority1) -> std::enable_if_t<std::is_floating_point_v<T>,void>
    {
        std::cout << "caught float/double : " << v << std::endl;
    }
    
    // For ranges
    template <class T> auto add(const T& range,priority1) -> decltype(begin(range),end(range),void())
    {
        for(auto const& v : range)
            *this << v;
    }
    
    // For chars
    void add(char c,priority1)
    {
        std::cout << c;
        std::cout.flush();
    }
    
    // When everything else fails ; ideally should never happen
    template <typename T> void add(const T& v,priority0)
    {
        assert(false && "should never happen");
    }
};

// The one data type. Notice how the primary template and the
// specialization have a `range` member of different types
template <class T> struct ValueOrRange
{
    struct Range
    {
        T min;
        T max;
    };
    Range range;
    T value;
};

template <> struct ValueOrRange<std::string>
{
    std::vector<std::string> range;
    std::string value;
};

// Overload operator<< for Catcher outside of the
// class to allow for processing of the new data type

// Also overload that for `ValueOrRange<T>::Range`. SFINAE should make sure
// that this behaves correctly (?)
template <class T> Catcher& operator<<(Catcher& c,const typename ValueOrRange<T>::Range& r)
{
    return c << r.min << r.max;
}

template <class T> Catcher& operator<<(Catcher& c,const ValueOrRange<T>& v)
{
    return c << v.range << v.value;
}

int main(int argc,char *argv[])
{
    ValueOrRange<std::string> vor1{{},"bleh"};
    ValueOrRange<float> vor2{{0.f,1.f},0.5f};
    
    Catcher c;
    c << vor1; // works fine,displays "bleh"
    c << vor2; // fails the assert in Catcher::add(const T&,priority0) with T = ValueOrRange<float>::Range
    return 0;
}

虽然行 c << vor1 通过各种重载正确解析并具有预期效果,但第二行 c << vor2 未通过断言。

  • 我想要发生的事情:c << vor2 调用 Catcher& operator<<(Catcher& s,const ValueOrRange<float>& v),后者又调用 Catcher& operator<<(Catcher& s,const typename ValueOrRange<float>::Range& r)
  • 会发生什么:不是 Catcher& operator<<(Catcher& s,const typename ValueOrRange<float>::Range& r),而是 Catcher& Catcher::operator<<(const T& v)T = typename ValueOrRange<float>::Range调用,因此断言失败。

值得注意的是,同样的代码在 MSVC 上具有预期的效果,但在 GCC 上没有断言。

知道我应该如何解决这个问题吗?

解决方法

Catcher& operator<<(Catcher& c,const typename ValueOrRange<T>::Range& r) 中,T 在不可推论中。

一种解决方法是 friend 函数:

template <class T> struct ValueOrRange
{
    struct Range
    {
        T min;
        T max;
        
        friend Catcher& operator<<(Catcher& c,const Range& r)
        {
            return c << r.min << r.max;
        }        
    };
    Range range;
    T value;
};

Demo