The Barton-Nackman Trick 介绍
  ZXL8ALkrtBPG 2023年11月02日 66 0
c++

问题说明

什么是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

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  gBkHYLY8jvYd   2023年11月19日   27   0   0 #includecic++
  ZXL8ALkrtBPG   2023年11月02日   67   0   0 c++
  ZXL8ALkrtBPG   2023年12月06日   23   0   0 字面量c++
  lh6O4DgR0ZQ8   2023年11月24日   18   0   0 cii++c++
  ZXL8ALkrtBPG   2023年11月12日   30   0   0 c++
  gBkHYLY8jvYd   2023年11月19日   27   0   0 十进制高精度c++
  ZXL8ALkrtBPG   2023年11月02日   51   0   0 c++
  ZXL8ALkrtBPG   2023年11月02日   52   0   0 c++
  gBkHYLY8jvYd   2023年12月11日   20   0   0 cic++最小值
  gBkHYLY8jvYd   2023年11月19日   24   0   0 测试点cic++
ZXL8ALkrtBPG