使用`enable_if`进行无限递归

时间:2013-08-06 06:05:07

标签: c++ operator-overloading template-meta-programming sfinae

为了尝试为另一种类型wrapper编写T类型,我遇到了一个相当令人讨厌的问题:我想定义一些转发的二元运算符(例如+wrapper对基础类型的任何操作,但我需要这些运算符接受涉及wrapper的任何潜在组合:

wrapper() + wrapper()
wrapper() + T()
T()       + wrapper()

天真的方法涉及直接编写所有潜在的重载。

但我不喜欢编写重复的代码并想要更多的挑战,所以我选择使用非常通用的模板来实现它,并使用enable_if来限制潜在的类型。

我的尝试显示在问题的底部(抱歉,这是我能想到的最小)。问题是它会遇到无限递归错误:

  1. 要评估test() + test(),编译会查看所有可能的重载。
  2. 此处定义的运算符实际上是潜在的重载,因此它尝试构造返回类型。
  3. 返回类型有一个enable_if子句,它应该阻止它成为有效的重载,但编译器只是忽略它并尝试首先计算decltype,这需要......
  4. ... operator+(test, test)的实例化。
  5. 我们回到了我们开始的地方。 GCC非常适合吐出错误; Clang只是段错误。

    对此有什么好的 clean 解决方案? (请记住,还有其他运营商需要遵循相同的模式。)

    template<class T>
    struct wrapper { T t; };
    
    // Checks if the type is instantiated from the wrapper
    template<class>   struct is_wrapper              : false_type {};
    template<class T> struct is_wrapper<wrapper<T> > : true_type  {};
    
    // Returns the underlying object
    template<class T> const T& base(const T& t)          { return   t; }
    template<class T> const T& base(const wrapper<T>& w) { return w.t; }
    
    // Operator
    template<class W, class X>
    typename enable_if<
        is_wrapper<W>::value || is_wrapper<X>::value,
        decltype(base(declval<W>()) + base(declval<X>()))
    >::type operator+(const W& i, const X& j);
    
    // Test case
    struct test {};
    int main() {
        test() + test();
        return 0;
    }
    

    这是一个相当笨重的解决方案,我宁愿不使用,除非我必须:

    // Force the evaluation to occur as a 2-step process
    template<class W, class X, class = void>
    struct plus_ret;
    template<class W, class X>
    struct plus_ret<W, X, typename enable_if<
        is_wrapper<W>::value || is_wrapper<X>::value>::type> {
        typedef decltype(base(declval<W>()) + base(declval<X>())) type;
    };
    
    // Operator
    template<class W, class X>
    typename plus_ret<W, X>::type operator+(const W& i, const X& j);
    

4 个答案:

答案 0 :(得分:2)

作为TemplateRex注释的补充,我建议使用宏来实现所有重载并将运算符作为参数:

template<class T>
struct wrapper { T t; };

#define BINARY_OPERATOR(op)                                      \
  template<class T>                                              \
  T operator op (wrapper<T> const& lhs, wrapper<T> const& rhs);  \
  template<class T>                                              \
  T operator op (wrapper<T> const& lhs, T const& rhs);           \
  template<class T>                                              \
  T operator op (T const& lhs, wrapper<T> const& rhs); 

BINARY_OPERATOR(+)
BINARY_OPERATOR(-)

#undef BINARY_OPERATOR

// Test case
struct test {};
test operator+(test const&, test const&);
test operator-(test const&, test const&);

int main() {
    test() + test();
    wrapper<test>() + test();
    test() - wrapper<test>();
    return 0;
}

答案 1 :(得分:2)

这是enable_if lazy_enable_ifdecltype(...)所触及的,在一种完全相似的情况下(尽管他们希望避免的错误是不同的)。 boost的解决方案是创建一个{{1}}类。

问题在于,编译器将尝试实例化函数签名中存在的所有类型,因此也会{{1}}表达式。也不能保证在类型之前计算条件。

不幸的是我无法找到解决这个问题的方法;我的最新尝试可以看到boost page并仍然触发最大的实例化深度问题。

答案 2 :(得分:1)

编写混合模式算法最直接的方法是遵循Scott Effective C++中的Scott Meyers的第24项

template<class T>
class wrapper1 
{ 
public:
    wrapper1(T const& t): t_(t) {} // yes, no explicit here

    friend wrapper1 operator+(wrapper1 const& lhs, wrapper1 const& rhs)
    {
        return wrapper1{ lhs.t_ + rhs.t_ };        
    }

    std::ostream& print(std::ostream& os) const
    {
        return os << t_;
    }

private:
    T t_; 
};

template<class T>
std::ostream& operator<<(std::ostream& os, wrapper1<T> const& rhs)
{
    return rhs.print(os);
}

请注意,您仍然需要编写operator+=才能提供一致的界面(&#34;执行注意&#34; )。如果您还想避免使用该样板,请查看Boost.Operators

template<class T>
class wrapper2
:
    boost::addable< wrapper2<T> >
{ 
public:
    wrapper2(T const& t): t_(t) {}

    // operator+ provided by boost::addable
    wrapper2& operator+=(wrapper2 const& rhs)
    {
        t_ += rhs.t_;
        return *this;
    }        

    std::ostream& print(std::ostream& os) const
    {
        return os << t_;
    }

private:
    T t_; 
};

template<class T>
std::ostream& operator<<(std::ostream& os, wrapper2<T> const& rhs)
{
    return rhs.print(os);
}

在任何一种情况下,您都可以写

int main()
{
    wrapper1<int> v{1};
    wrapper1<int> w{2};

    std::cout << (v + w) << "\n";
    std::cout << (1 + w) << "\n";
    std::cout << (v + 2) << "\n";

    wrapper2<int> x{1};
    wrapper2<int> y{2};

    std::cout << (x + y) << "\n";
    std::cout << (1 + y) << "\n";
    std::cout << (x + 2) << "\n";

}

将在所有情况下打印3。 Live example。 Boost方法非常普遍,例如您可以从boost::arithmetic派生,也可以从operator*的定义中提供operator*=

注意:此代码依赖于Twrapper<T>的隐式转换。但引用Scott Meyers的话说:

  

支持隐式类型转换的类通常是个坏主意。   当然,这个规则有例外,也是最常见的规则之一   常见的是在创建数字类型时。

答案 3 :(得分:1)

我为你的目的找到了一个更好的答案:不要让它变得复杂,不要使用太多的元编程。而是使用简单的函数来解包和使用普通表达式。您不需要使用enable_if从函数重载集中删除操作符。如果不使用它们,则永远不需要compile,如果使用它们,则会提供有意义的error

namespace w {
    template<class T>
    struct wrapper { T t; };

    template<class T> 
    T const& unwrap(T const& t) {
        return t;
    }

    template<class T>
    T const& unwrap(wrapper<T> const& w) {
        return w.t;
    }

    template<class T1,class T2>
    auto operator +(T1 const& t1, T2 const& t2) -> decltype(unwrap(t1)+unwrap(t2)) {
        return unwrap(t1)+unwrap(t2);
    }
    template<class T1,class T2>
    auto operator -(T1 const& t1, T2 const& t2) -> decltype(unwrap(t1)-unwrap(t2)) {
        return unwrap(t1)-unwrap(t2);
    }

    template<class T1,class T2>
    auto operator *(T1 const& t1, T2 const& t2) -> decltype(unwrap(t1)*unwrap(t2)) {
        return unwrap(t1)*unwrap(t2);
    }
}    

// Test case
struct test {};

test operator+(test const&, test const&);
test operator-(test const&, test const&);

int main() {
    test() + test();
    w::wrapper<test>() + w::wrapper<test>();

    w::wrapper<test>() + test();
    test() - w::wrapper<test>();

    return 0;
}

修改

作为一个有趣的附加信息我不得不说,来自fzlogic的orignal soultion在msvc 11下编译(但不是10)。现在我的解决方案(这里介绍)不能在两者上编译(给出C1045)。如果你需要用msvc和gcc来解决这些问题(我这里没有clang)你必须将逻辑移动到不同的命名空间和函数,以防止编译器使用ADL并考虑模板operator+。 / p>