首页 > 其他分享 >The Barton-Nackman Trick 介绍

The Barton-Nackman Trick 介绍

时间:2023-03-26 10:31:53浏览次数:48  
标签:const val Val ValWrapper Barton wrapper Trick Nackman reference

问题说明

什么是The Barton-Nackman Trick?

1994年,Barton和Nackman提出的一种模板编程技巧,由于当时模板函数重载和命名空间等机制不完善的背景下提出的一种技巧,现在通常配合CRTP进行设计;

例如自定义类型ValWrapper需要实现equality操作,常见的方法如下,定义为类内成员函数:

template<typename T>
struct ValWrapper {
    ValWrapper(T v) : m_val(v) {}
    bool operator == (const ValWrapper& v) const
    {   
        return (m_val == v.m_val);
    }   
    T m_val;
};
TEST(testCase, Default) 
{
    ValWrapper v(10);
    EXPECT_EQ(v == 10, true);
}

上述代码中operator== 隐式绑定了this指针作为第一个入参,即在编译器看来为:

bool operator == (this, const ValWrapper& v) const {...}

因此作为成员函数实现的operator == 操作缺少对称性,即:

EXPECT_EQ(10 == v, true); // 编译失败

因此修改为:

template<typename T>
struct ValWrapper {
    ValWrapper(T v) : m_val(v) {}
    T m_val;
};

bool operator == (const ValWrapper<int>& v1, const ValWrapper<int>& v2)
{
    return (v1.m_val == v2.m_val);
}

TEST(testCase, Default) 
{
    ValWrapper v(10);
    EXPECT_EQ(v == 10, true);
    EXPECT_EQ(10 == v, true);
}

此时 “==” 操作是对称的,但这样又有个问题,全局operator == 形参类型固定,当T需要扩展为其他类型时则无法找到对应函数,进而自然想到修改为泛化版本即:

template<typename T>
bool operator == (const ValWrapper<T>& v1, const ValWrapper<T>& v2)
{
    return (v1.m_val == v2.m_val);
}

但现实是不尽人意,此时int 到ValWrapper<T>隐式转换失败而无法找到对应函数。

如何解决?重载定义一个对应类型的operator==函数显然不是一个好办法;

解决方案

Trick实现:

template<typename T>
struct ValWrapper {
    friend bool operator == (const ValWrapper& v1, const ValWrapper& v2)
    {
        return (v1.m_val == v2.m_val);
    }
    ValWrapper(T v) : m_val(v) {}
    T m_val;
};
   
   
TEST(testCase, Default) 
{   
    ValWrapper v(10);
    EXPECT_EQ(v == 10, true);
    EXPECT_EQ(10 == v, true);
        
    ValWrapper<char> vc('A');
    EXPECT_EQ(vc == 'A', true);
    EXPECT_EQ('A' == vc, true);
}

此时上述提到的问题都得到解决,而在类中定义友元函数即为“The Barton-Nackman Trick”,这里类内友元函数能添加到函数重载决议得益于ADL,具体请参见笔者之前的博文介绍【1】;

再改造上面的例子:

template<typename T>
struct Val {
    explicit Val(T val): m_val(val) {}
    T m_val;
};
   
template<typename T>
bool operator == (const Val<T> &v1, const Val<T> &v2)
{
    return v1.m_val == v2.m_val;
}

TEST(testCase, Default) 
{   
    Val v1(10);
    Val v2(10);
    EXPECT_EQ(v1 == v2, true); // compile
}

Val使用explicit取消了隐式转换,目前为止编译工作正常;

这时Val配合std::reference_wrapper使用:

template<typename T>
struct Val { 
    explicit Val(T val): m_val(val) {}
    T m_val;
}; 

template<typename T>
bool operator == (const Val<T> &v1, const Val<T> &v2)
{   
    return v1.m_val == v2.m_val;
}  

TEST(testCase, Default)
{   
    Val v1(10);
    Val v2(10);
    std::reference_wrapper<Val<int>> ref1(v1);
    std::reference_wrapper<Val<int>> ref2(v2);
    EXPECT_EQ(ref1 == ref2, true); // compile error
}

此时编译失败,找不到匹配的函数;因为类型std::reference_wrapper<Val<int>>无法隐式转换到Val<T>,将Val同样使用Trick修改:

template<typename T>
struct Val {
    friend bool operator == (const Val& v1, const Val& v2)
    {
        return (v1.m_val == v2.m_val);
    }
    explicit Val(T val): m_val(val) {}
    T m_val;
}; 

TEST(testCase, Default)
{   
    Val v1(10);
    Val v2(10);
    std::reference_wrapper<Val<int>> ref1(v1);
    std::reference_wrapper<Val<int>> ref2(v2);
    EXPECT_EQ(ref1 == ref2, true); // compile ok
}

编译成功,而原因本质上和上文中的ValWrapper的例子相同,结合std::reference_wrapper源码【3】

namespace detail {
template <class T> constexpr T& FUN(T& t) noexcept { return t; }
template <class T> void FUN(T&&) = delete;
}
 
template <class T>
class reference_wrapper
{
public:
    // types
    using type = T;
 
    // construct/copy/destroy
    template <class U, class = decltype(
        detail::FUN<T>(std::declval<U>()),
        std::enable_if_t<!std::is_same_v<reference_wrapper, std::remove_cvref_t<U>>>()
    )>
    constexpr reference_wrapper(U&& u)
        noexcept(noexcept(detail::FUN<T>(std::forward<U>(u))))
        : _ptr(std::addressof(detail::FUN<T>(std::forward<U>(u)))) {}
 
    reference_wrapper(const reference_wrapper&) noexcept = default;
 
    // assignment
    reference_wrapper& operator=(const reference_wrapper& x) noexcept = default;
 
    // access,类型转换
    constexpr operator T& () const noexcept { return *_ptr; }
    constexpr T& get() const noexcept { return *_ptr; }
 
    template< class... ArgTypes >
    constexpr std::invoke_result_t<T&, ArgTypes...>
        operator() ( ArgTypes&&... args ) const
            noexcept(std::is_nothrow_invocable_v<T&, ArgTypes...>)
    {
        return std::invoke(get(), std::forward<ArgTypes>(args)...);
    }
 
private:
    T* _ptr;
};
 
// deduction guides
template<class T>
reference_wrapper(T&) -> reference_wrapper<T>;

可以发现源码中包含了类型转换的重载实现,即std::reference_wrapper<Val<int>> 可以隐式转换为Val<int>,同时在"ef1 == ref2" 编译时,由于std::reference_wrapper的模板实参为Val<int>,应用ADL可以搜索到类Val<int>中的友函数实现参与重载决议(具体的实例可以参见【1】),因此operator==(...) + argument implicit conversion导致上述编译成功;

std::reference_wrapper<Val<int>> ref1(v1);
    std::reference_wrapper<Val<int>> ref2(v2);  
    // 模板参数为Val<int>,应用ADL,可以搜索到类Val<int>中的友函数实现参与重载决议
    EXPECT_EQ(ref1 == ref2, true); // compile ok

所以使用Trick就可以避免上述的这些问题;

综上:根据业务场景,类型operator操作符实现,推荐使用类内友元;

最后举一个文章开头所述的配合CRTP进行设计【4】:

template<typename Derived>
class EqualityComparable
{
    public:
         // 公共操作提取到基类
        friend bool operator!=(Derived const& x1,Derived const x2)
        {
            return !(x1==x2);
        }
};
class X:public EqualityComparable<X>
{
    public:
        friend bool operator==(X const& x1,X const& x2)
        {
            //比较并返回结果
        }
};

int main()
{
    X x1,x2;
    if(x1!=x2){}
}

参考资料

【1】《C++ ADL & CPO介绍》

【2】An example of the Barton–Nackman trick

【3】https://en.cppreference.com/w/cpp/utility/functional/reference_wrapper

【4】C++ templates

标签:const,val,Val,ValWrapper,Barton,wrapper,Trick,Nackman,reference
From: https://blog.51cto.com/u_13137973/6149865

相关文章

  • 常见trick总结
    记录一些做题时遇到的有价值的trick。CF1717E\[a+b=n\]\[\gcd(a,b)=\gcd(a,a+b)=\gcd(a,n)=\varphi(n)\]P2619二分\(\Delta\),每条白边加上\(\Delta\)求\(\te......
  • Trick:最小环
    求无负环无向图至少具有三个节点的最小简单环,考虑最小的简单环\(x_1x_2x_3\cdotsx_nx_1,n\ge3\),不妨令\(x_n=max\{x_i\}\),那么显然有\(x_1x_2\cdotsx_{n-1}\)是只经......
  • 常见 Trick
    二维凸包的点数在随机情况下是\(O(\logn)\)的。树的高度在随机情况下是\(O(\logn)\)的。要求最大值最小值的时候,有三个方向:min-max容斥,二分,直接求。条件很复杂......
  • pairwise损失_triplet损失_提升精排模型的trick
    多标签importtorchimporttorch.nnasnnimporttorch.optimasoptimclassRankModel(nn.Module):def__init__(self,num_features):super(RankMode......
  • A. Amazing Trick
    A.AmazingTrick思路对于p数组,每个数只有两个禁止的位置不能是自己,并且a[pi]也不是。也就是每个点限制有两个位置不能放,可行的种类有很多,但是模拟起来又较复杂,所以采......
  • 开发者进阶必备的9个Tips & Tricks!
    优秀的开发人员市场前景是十分广阔的,但想找到一份理想的工作,仅有代码知识是不够的。优秀的工程师应该是一个终身学习者、问题的创造性解决者,着迷于整个软件世界。要成为一......
  • BJGK Che's Trick
    神秘把读题里“有效的信息”圈出来,务必要体现在答案里亚硫磷亚硝;氟乙碳氢硫;次氯氢氰硅;苯酚碳氢铝过滤完,固体蘸着液体,一定要洗涤。电泳,交替,可以吸附正负离子。......
  • Slope Trick
    原理若一个函数满足:连续分段线性凸性则可以使用SlopeTrick来快速维护。我们发现我们可以仅通过记录转折点,转折点处斜率变化,以及一侧的直线即可维护出整个函数。......
  • Slope Trick
    定义本质上是用一个二元组\((S,f(x))\)描述由一堆直线构成的分段函数去优化dp,要求这些分段函数满足凸性。其中\(S\)是一个可重集,\(f(x)\)是一个一次函数。我们定义......
  • Little Useless Trick
    记录一下近期收集到的没用的小\(trick\)。并查集合并树我们考虑去维护这样一种操作:1xy给\(x\)和\(y\)之间连一条无向边。2x询问\(x\)所在的连通块内点的......