问题说明
什么是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