如何让tag_invoke CPO统一处理匹配元谓词的类型与reference_wrapper包装类型?
std::reference_wrapper的通用解决方案 需求背景
核心需求是让自定义的定制点对象(CPO)既能处理Foo<N>类型,也能无缝兼容std::reference_wrapper<Foo<N>>,同时避免为每个CPO单独编写reference_wrapper的重载实现(减少重复代码)。优先关注C++17解决方案(可自行实现std::unwrap_ref_decay逻辑),同时了解C++20的简化写法。
现有代码与问题
1. CPO定义
constexpr struct CPO { template<typename T> constexpr auto operator()(T&& t) const -> decltype(tag_invoke(*this, std::forward<T>(t))) { return tag_invoke(*this, std::forward<T>(t)); } } cpo{};
2. 模板类Foo
template<std::size_t N> struct Foo { constexpr int& cpo() { return i; } constexpr int const& cpo() const { return i; } int i; };
3. 初始tag_invoke实现(存在缺陷)
最初针对Foo<N>的实现不仅有拷贝问题,还无法兼容std::reference_wrapper<Foo<N>>:
template<std::size_t N> constexpr auto tag_invoke(CPO, Foo<N> foo) { return foo.cpo(); }
- 传入
std::ref(foo)时编译失败,因为没有匹配的tag_invoke重载; - 即便单独针对特定
Foo<N>实例实现兼容逻辑,泛化到所有N时依然无法自动转换。
4. 当前解决方案的局限
通过自定义is_foolike_v元谓词筛选类型,结合std::unwrap_ref_decay处理的方案虽能运行,但存在硬编码reference_wrapper逻辑的问题;在类似std::array提取首元素的场景中,赋值操作(如cpo(arr)=3)会失败,需用if constexpr区分类型,实现繁琐:
template <typename T, std::enable_if_t<is_foolike_v<T>, bool> = true> constexpr decltype(auto) tag_invoke(CPO, T&& foolike) { return std::unwrap_ref_decay_t<T>(std::forward<T>(foolike)).cpo(); }
C++17优化方案
核心思路是:先实现通用的解引用工具,再通过一层转发重载,将所有可解包为Foo<N>的类型(包括reference_wrapper)转发给针对原始Foo<N>的完美转发实现,避免重复代码。
1. 实现C++17版unwrap_ref_decay
标准库C++17未提供该工具,自行实现:
#include <type_traits> #include <functional> template<typename T> constexpr auto unwrap_ref_decay(T&& t) noexcept { using U = std::decay_t<T>; if constexpr (std::is_same_v<U, std::reference_wrapper<std::remove_reference_t<T>>>) { return t.get(); } else { return std::forward<T>(t); } }
2. 针对Foo<N>的完美转发tag_invoke
先解决拷贝问题,为Foo<N>的所有值类别实现重载:
template<std::size_t N> constexpr decltype(auto) tag_invoke(CPO, Foo<N>& foo) noexcept { return foo.cpo(); } template<std::size_t N> constexpr decltype(auto) tag_invoke(CPO, const Foo<N>& foo) noexcept { return foo.cpo(); } template<std::size_t N> constexpr decltype(auto) tag_invoke(CPO, Foo<N>&& foo) noexcept { return std::move(foo).cpo(); } template<std::size_t N> constexpr decltype(auto) tag_invoke(CPO, const Foo<N>&& foo) noexcept { return std::move(foo).cpo(); }
3. 通用转发重载(兼容所有可解包为Foo<N>的类型)
首先定义元谓词判断类型是否可解包为Foo<N>:
template<typename T> struct is_foo : std::false_type {}; template<std::size_t N> struct is_foo<Foo<N>> : std::true_type {}; template<typename T> constexpr bool is_foo_v = is_foo<T>::value; template<typename T> constexpr bool is_unwrappable_to_foo_v = is_foo_v<std::decay_t<decltype(unwrap_ref_decay(std::declval<T>()))>>;
然后编写通用转发的tag_invoke重载:
template<typename T> constexpr std::enable_if_t<is_unwrappable_to_foo_v<T>, decltype(auto)> tag_invoke(CPO, T&& t) noexcept { auto&& unwrapped = unwrap_ref_decay(std::forward<T>(t)); return tag_invoke(cpo, std::forward<decltype(unwrapped)>(unwrapped)); }
该重载会自动处理std::reference_wrapper<Foo<N>>,同时完美转发值类别,支持赋值操作(如cpo(std::ref(foo)) = 5)。
C++20简化方案
C++20标准库提供了std::unwrap_ref_decay,同时可通过Concept简化元谓词,代码更简洁。
1. 定义FooLike Concept
#include <type_traits> #include <functional> template<typename T> struct is_foo : std::false_type {}; template<std::size_t N> struct is_foo<Foo<N>> : std::true_type {}; template<typename T> concept FooLike = is_foo<std::unwrap_ref_decay_t<T>>::value;
2. 简化的通用tag_invoke重载
template<FooLike T> constexpr decltype(auto) tag_invoke(CPO, T&& t) noexcept { auto&& unwrapped = std::unwrap_ref_decay(std::forward<T>(t)); return tag_invoke(cpo, std::forward<decltype(unwrapped)>(unwrapped)); }
3. C++20风格的CPO定义(可选)
用requires表达式替代decltype,写法更直观:
constexpr struct CPO { template<typename T> constexpr decltype(auto) operator()(T&& t) const requires requires { tag_invoke(*this, std::forward<T>(t)); } { return tag_invoke(*this, std::forward<T>(t)); } } cpo{};
内容的提问来源于stack exchange,提问作者Enlico

