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

Compiler differences with use of templated overloads

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

#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 断言失败。

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

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

感谢 Igor Tandetnik 的反馈,我摆脱了 ::Range 特定的过载,只是去检查 std::is_same_v<T, std::string>。模块化比我想要的要少一些,但它可以解决问题。

// A single function will do the trick
template <class T> Catcher& operator<<(Catcher& c, const ValueOrRange<T>& v)
{
    if constexpr (std::is_same_v<T, std::string>)
        c << v.range;
    else
        c << v.range.min << v.range.max;
    return c << v.value;
}

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