【问题标题】:C++ templates: Specialized member function to resolve case of ambiguous overload in primary templateC++ 模板:用于解决主模板中不明确重载情况的专用成员函数
【发布时间】:2014-10-30 10:22:59
【问题描述】:

我有一个模板,其中一个函数被重载,因此它可以处理std::string 参数和模板实例化的参数类型。这工作正常,除非模板是用std::string 实例化的,因为这会导致两个成员函数具有相同的原型。因此,我选择专门针对这种特殊情况使用该功能。然而,似乎编译器(g++ 4.8.1 带有标志-std=c++0x)从来没有达到特化实际上覆盖主模板的地步,并且它在似乎意识到它应该使用特化之前抱怨模棱两可的重载.有没有办法解决这个问题?

#include <iostream>

template<class T>
struct A {
    std::string foo(std::string s) { return "ptemplate: foo_string"; }
    std::string foo(T e) { return "ptemplate: foo_T"; }
};

template<> //Error!
std::string A<std::string>::foo(std::string s) { return "stemplate: foo_string"; }

int main() {
    A<int> a; //Ok!
    std::cout << a.foo(10) << std::endl; 
    std::cout << a.foo("10") << std::endl;

    //A<std::string> b; //Error!
    //std::cout << a.foo("10") << std::endl;
    return 0;
}

这会导致编译错误,即使我根本不使用std::string 进行实例化(似乎编译器在看到专业化之后就使用std::string 进行实例化,并且在它实际处理专业化之前,抱怨专业化反过来会“消除歧义”的模棱两可的过载)。

编译器输出:

p.cpp: In instantiation of 'struct A<std::basic_string<char> >':
p.cpp:10:27:   required from here
p.cpp:6:14: error: 'std::string A<T>::foo(T) [with T = std::basic_string<char>; std::string = std::basic_string<char>]' cannot be overloaded
  std::string foo(T e) { return "ptemplate: foo_T"; }
              ^
p.cpp:5:14: error: with 'std::string A<T>::foo(std::string) [with T = std::basic_string<char>; std::string = std::basic_string<char>]'
  std::string foo(std::string s) { return "ptemplate: foo_string"; }
              ^

我希望它在主模板中跳过foo() 的实现并使用专业化而不考虑主模板foo()。是否可以以某种方式完成,可能使用非类型模板参数,或者我是否必须为 std::string 制作一个完全专业化的类模板,其中包含它所暗示的所有代码重复(我不喜欢在这里使用继承)......其他建议?

【问题讨论】:

    标签: c++ templates overloading template-specialization


    【解决方案1】:

    当你指定你的成员函数时,你仍然会得到双重模糊声明。您需要专门化结构模板:

    template<>
    struct A<std::string> {
        std::string foo(std::string s) { return "ptemplate: foo_string"; }
    };
    

    如果A 结构有很多成员,也许你可以重构:

    template<typename T>
    struct Afoo
    {
        std::string foo(T s) { ... }
        std::string foo(std::string s) { ... }
    };
    template<>
    struct Afoo<std::string>
    {
        std::string foo(std::string s) { ... }
    };
    template<typename T>
    struct A : Afoo<T>
    {
        //a lot of code
    };
    

    【讨论】:

    • 啊哈!但是我需要重新声明该专业化中的所有其他成员函数和变量,对吗?另外,以我实际做的方式添加专业化是什么?如果不是因为重载,编译器会接受它,但我可以看到它不会产生任何结果......
    • @fast-reflexes:是的,你可能有很多重复的代码,除非你重构你的代码(想想Afooer 基类?)。
    • @fast-reflexes:好的,我在纠正自己,我的关于函数模板专业化的 cmets 是无关紧要的,因为你的函数本身不是模板,而是模板结构的函数成员。你可以特化一个模板成员函数,但除非你特化结构,否则你不会避免双重声明。
    • 是的,我只是做了一些尝试,并且以这种方式专门化成员函数效果很好......好吧,那么我们回到我们开始的地方......我只是想让编译器在这种情况下忽略主模板,因为它会导致歧义,这将由专业化解决,但我想你是对的,只要我使用相同的模板,就会有歧义,因为编译器首先检查主模板,然后专业化...我认为 nyarlathotep108s 的答案可能有用...
    • @fast-reflexes:不完全是,编译器会首先检查A 中的成员函数声明,并由于重复重载而失败。成员函数特化不能改变这一点:它改变了函数的定义,而不是声明
    【解决方案2】:

    我将自己回答这个问题,因为我今天一直在深入研究这个主题,我认为这些解决方案很好。到目前为止,所有其他帖子都做出了贡献,并且具有在其他情况下具有潜力的有吸引力的细节。但是,我更愿意考虑这些事情:

    • 避免使用多个类模板
    • 尽可能避免过于复杂的专业化
    • 避免使用继承和重构基类和派生类
    • 避免使用额外的包装器

    在我接受它作为我的答案之前,请随时发表评论。

    另一篇关于成员函数重载而不是特化的好文章和鼓舞人心的文章可以在explicit specialization of template class member function找到。

    解决方案 1

    template<class T>
    struct A {
        template<class V = T> std::string foo(T) { return "foo_T"; }
        std::string foo(std::string) { return "foo_std::string"; }
        std::string foo(const char *) { return "foo_const char *"; }
    };
    
    template<> template<> 
    std::string A<std::string>::foo(std::string s) { return foo(s); }
    

    我认为这是一个密集且易于理解的解决方案,允许所有类实例化使用foo(std::string)foo(const char *)(用于将字符串作为右值传递)。虚拟模板参数的使用有效地阻止了带有std::string 的类实例化导致模棱两可的重载,同时实际模板参数阻碍了具有不可预测函数参数的不受控制的函数实例化。唯一的问题可能来自使用std::string 的类实例化,如果使用foo&lt;std::string&gt;(std::string) 显式调用它可能使用模板而不是常规成员函数,我希望该类使用常规foo(std::string) 而不是函数其他实例化的模板。这可以通过使用单个模板特化来解决。

    解决方案 2

    template<class T>
    struct A {
            template<class V> std::string foo(V s) { return foo_private(s); } 
    
        private:
            template<class V = T> std::string foo_private(T) { return "foo_T"; }
            std::string foo_private(const char *) { return "foo_const char *"; }
            std::string foo_private(std::string) { return "foo_std::string"; }
    };
    

    这个版本允许我们跳过特化,以便在类声明中使用第二个模板。

    两个版本都用于:

    int main() {
        A<int> a;
        std::cout << a.foo(10) << std::endl; 
        std::cout << a.foo("10") << std::endl;
    
        A<std::string> b;
        std::cout << b.foo<std::string>("10") << std::endl;
        std::cout << b.foo("10") << std::endl;
    
        return 0;
    }
    

    ...输出:

    foo_T
    foo_const char *
    foo_const char *
    foo_std::string
    

    【讨论】:

    • 我喜欢虚拟参数的想法。绝对比我的解决方案更好:-)
    【解决方案3】:

    错误是说您最终创建了两个具有相同签名的方法。 这是因为该结构已使用 std::string 作为参数进行模板化。

    您应该将函数作为模板函数,使用其自己的模板参数“K”,与结构模板参数“T”无关。这样就可以只针对函数实现模板特化了。

    【讨论】:

    • 这太笨拙了。您总是希望 KT 相同,所以允许它们不同是愚蠢的!
    • @LightnessRacesinOrbit 没什么傻事,拥有模板结构和模板函数是完全正常的。这取决于您想要实现的目标。
    • 做那种极其广泛的事情并不傻,不。不过,这不是一个适用的解决方案。
    • 我其实并不认为这是愚蠢的,只要你考虑后果并限制模板化函数的使用(因此它不会导致任何不可预测的行为,例如只期望与类实例化类型相同,但允许以不受控制的方式传递任何内容)
    【解决方案4】:

    我承认我在下面提供的解决方案确实是一个 hacky 解决方案,但它确实完成了您想要做的事情,而且有点有趣。使用前请仔细考虑;-)

    我通过创建一个名为FakeType 的新类型来解决此问题,该类型可以从您的模板类型T 构造。 foo 的第二个重载现在是 FakeType&lt;T&gt; 而不是 T,所以即使 T == string 也会有两个不同的重载:

    template <typename T>
    struct FakeType
    {
        T t;
        FakeType(T const &t_): t(t_) {}
        operator T() { return t; }
    };
    
    template <typename T>
    struct A
    {
        string foo(string s) { return "ptemplate: foo_string"; }
        string foo(FakeType<T> e) { return "ptemplate: foo_T"; }
    };
    

    对于T != string的情况:

    A<int>().foo("string"); // will call foo(string s)
    A<int>().foo(1); // will call foo(FakeType<int> e)
    

    在后一种情况下,int 将被提升为FakeType&lt;int&gt;,可以通过转换运算符将其用作常规int

    对于T == string的情况:

    A<string>().foo("string"); // will still call foo(string s)
    

    因为编译器总是更喜欢不需要提升的重载。

    PS。这种方法假定foo 将通过值或常量引用获取其参数。只要您尝试通过引用传递,它就会中断(这可以修复)。

    【讨论】:

    • 除了被称为 FakeType 的类型之外,我认为它并没有那么糟糕 :) 我喜欢这个解决方案! FakeType 上的 operator() 只是为了方便,对吧?在经过一些询问后,我将通过我自己的答案来回答这个问题,如果有任何问题,我会很高兴听到它。谢谢!
    • @fast-reflexes 很高兴你喜欢它!是的,operator T() 是为了方便。它允许您像使用int 一样使用FakeType&lt;int&gt;。这就像一个通过引用返回成员的 get 方法。如果您需要更多帮助(例如,当您希望 foo 方法通过非常量引用接受其参数时),请告诉我。另外,请注意,这样的实现需要复制操作来构造FakeType&lt;&gt;,因此它可能不适合性能关键应用程序中的大型类型。这里也有一些改进,但我还没有包括这些。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多