6

In an attempt to write a wrapper type for another type T, I encountered a rather obnoxious problem: I would like to define some binary operators (such as +) that forward any operation on wrapper to the underlying type, but I need these operators accept any of the potential combinations that involve wrapper:

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

The naive approach involves writing all the potential overloads directly.

But I don't like writing duplicated code and wanted a bit more challenge, so I chose to implement it using a very generic template and restrict the potential types with an enable_if.

My attempt is shown at the bottom of the question (sorry, this is as minimal as I can think of). The problem is that it will run into an infinite recursion error:

  1. To evaluate test() + test(), the compile looks at all potential overloads.
  2. The operator defined here is in fact a potential overload, so it tries to construct the return type.
  3. The return type has an enable_if clause, which is supposed to prevent it from being a valid overload, but the compiler just ignores that and tries to compute the decltype first, which requires ...
  4. ... an instantiation of operator+(test, test).

And we're back where we started. GCC is nice enough to spit an error; Clang just segfaults.

What would be a good, clean solution for this? (Keep in mind that there are also other operators that need to follow the same pattern.)

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;
}

Here's rather clunky solution that I would rather not use unless I have to:

// 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

4 回答 4

2

这是在 的提升页面上提到的enable_if,在一个非常相似的情况下(尽管他们希望避免的错误是不同的)。boost 的解决方案是创建一个lazy_enable_if类。

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

不幸的是,我无法想出解决这个问题的办法;我的最新尝试可以在这里看到并且仍然触发最大实例化深度问题。

于 2013-08-06T07:05:41.063 回答
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;
}
于 2013-08-06T06:59:04.090 回答
1

I have a better answer for your purpose: don't make it to complicated, don't use to much meta programming. Instead use simple function to unwrap and use normal expressions. You don't need to use enable_if to remove to operators from function overload set. If they are not used the will never need to compile and if the are used they give a meaningfull 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;
}

Edit:

As an intersting additional information I have to say, the orignal soultion from fzlogic compiles under msvc 11 (but not 10). Now my solution (presented here) doesn't compile on both (gives C1045). If you need to address these isues with msvc and gcc (I don't have clang here) you have to move logic to different namespaces and functions to prevent the compiler to uses ADL and take the templated operator+ into consideration.

于 2013-08-06T07:26:01.377 回答
1

编写混合模式算术最直接的方法是遵循 Scott Meyers 在Effective C++中的第 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+=以提供一致的接口(“do as the ints do”)。如果您还想避免使用该样板,请查看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 。活生生的例子。Boost 方法非常通用,例如,您boost::arithmetic可以operator*operator*=.

注意:此代码依赖于 to 的隐式T转换wrapper<T>。但引用斯科特迈耶斯的话:

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

于 2013-08-06T07:10:56.753 回答