...如何根据模板参数的值用不同的内容填充模板类?
很久以来,考虑到C ++的经验,D语言就开始被制造为“正确的C ++”。 随着时间的流逝,D已变得比C ++复杂,表达能力更强的语言。 并且C ++已经开始监视D。例如,在我看来, if constexpr
是直接从D借来的,则C ++ 17中if constexpr
出现了,D的原型是D-shny static if 。
不幸的是, if constexpr
C ++中的if constexpr
与D中的static if
函数不具有相同的功能。这是有原因的 ,但是在某些情况下,您只能后悔if constexpr
C ++中的if constexpr
不允许您控制C +的内容+类。 我想谈谈其中一种情况。
我们将讨论如何制作一个模板类,其内容(即方法的组成和某些方法的逻辑)将根据传递给该模板类的参数而改变。 从现实生活中以开发SObjectizer新版本的经验为例 。
要解决的任务
需要创建用于存储消息对象的“智能指针”的精巧版本。 这样您就可以编写如下内容:
message_holder_t<my_message> msg{ new my_message{...} }; send(target, msg); send(another_target, msg);
这个message_holder_t
类的技巧是要考虑三个重要因素。
消息的类型是什么?
参数化message_holder_t
的消息类型分为两组。 第一组是从特殊基本类型message_t
继承的消息。 例如:
struct so5_message final : public so_5::message_t { int a_; std::string b_; std::chrono::milliseconds c_; so5_message(int a, std::string b, std::chrono::milliseconds c) : a_{a}, b_{std::move(b)}, c_{c} {} };
在这种情况下,自身内部的message_holder_t应该只包含一个指向此类型对象的指针。 相同的指针应在getter方法中返回。 也就是说,对于来自message_t
的继承人message_t
应该有类似以下内容:
template<typename M> class message_holder_t { intrusive_ptr_t<M> m_msg; public: ... const M * get() const noexcept { return m_msg.get(); } };
第二组是不从message_t
继承的任意用户类型的消息。 例如:
struct user_message final { int a_; std::string b_; std::chrono::milliseconds c_; user_message(int a, std::string b, std::chrono::milliseconds c) : a_{a}, b_{std::move(b)}, c_{c} {} };
SObjectizer中这些类型的实例不是自己发送的,而是封装在特殊的包装中,即user_type_message_t<M>
,该包装已从message_t
继承。 因此,对于此类类型, message_holder_t
必须在其中包含指向user_type_message_t<M>
的指针,而getter方法必须返回指向M的指针:
template<typename M> class message_holder_t { intrusive_ptr_t<user_type_message_t<M>> m_msg; public: ... const M * get() const noexcept { return std::addressof(m_msg->m_payload); } };
消息的抗扰性或可变性
第二个因素是将消息分为不可变和可变的。 如果消息是不可变的(默认情况下是不可变的),则getter方法必须返回指向消息的常量指针。 如果是可变的,则获取器必须返回一个非恒定指针。 即 应该是这样的:
message_holder_t<so5_message> msg1{...};
shared_ptr和unique_ptr
第三个因素是message_holder_t
作为智能指针的行为的逻辑。 一旦它应该表现得像std::shared_ptr
,即 您可以有多个message_holders引用同一个消息实例。 并且一旦它的行为就像std::unique_ptr
,即 只有一个message_holder实例可以引用一个消息实例。
默认情况下, message_holder_t
的行为应取决于消息的可变性/不可变性。 即 对于不可变消息, message_holder_t
行为应类似于std::shared_ptr
,对于可变std::unique_ptr
如std::unique_ptr
:
message_holder_t<so5_message> msg1{...}; message_holder_t<so5_message> msg2 = msg;
但是生活是一件复杂的事情,因此您还需要能够手动设置message_holder_t
行为。 这样就可以使message_holder成为行为不变的消息,其行为类似于unique_ptr。 这样您就可以使message_holder用于行为类似于shared_ptr的可变消息:
using unique_so5_message = so_5::message_holder_t< so5_message, so_5::message_ownership_t::unique>; unique_so5_message msg1{...}; unique_so5_message msg2 = msg1;
因此,当message_holder_t
像shared_ptr一样工作时,它应该具有通常的构造函数和赋值运算符集:复制和移动。 此外,必须有一个常量方法make_reference
,该方法返回存储在message_holder_t
内部的指针的副本。
但是,当message_holder_t
像unique_ptr一样工作时,则应禁止使用构造函数和复制运算符。 并且make_reference
方法应该从message_holder_t
对象获取指针:调用make_reference
原始message_holder_t
应该保持为空。
因此,您需要创建一个模板类:
template< typename M, message_ownership_t Ownership = message_ownership_t::autodetected> class message_holder_t {...};
其中:
- 应该将
intrusive_ptr_t<M>
或intrusive_ptr<user_type_message_t<M>>
存储在内部,具体取决于M是否从message_t
继承而来; - getter方法必须根据消息的可变性/不可变性返回
const M*
或M*
。 - 应该有一组完整的构造函数和复制/移动运算符,或者只有一个构造函数和移动运算符
make_reference()
方法应返回存储的intrusive_ptr的副本,或者应采用intrusive_ptr的值并将原始message_holder_t
保留为空。 在第一种情况下, make_reference()
必须为常量,在第二种方法-非常量方法中。
列表中的最后两项是由Ownership参数确定的(如果所有权autodetected
使用autodetected
的话,还有消息的可变性)。
如何决定
在本节中,我们将考虑构成最终解决方案的所有组件。 好吧,最终的解决方案本身。 将显示清除了所有令人分心细节的代码片段。 如果有人对真实代码感兴趣,那么可以在此处查看 。
免责声明
下面显示的解决方案并不装作精美,理想或榜样。 在截止日期的压力下,可以在短时间内找到,实施,测试和记录该文档。 也许如果有更多的时间,并且有更多的人在寻找解决方案, 年轻的 在现代C ++开发人员中明智且博学,它将变得更加紧凑,更简单和更易于理解。 但是,事实证明,它发生了……一般来说,“不要射击钢琴家”。
步骤顺序和现成的模板魔术
因此,我们需要一个带有几组方法的类。 这些工具包中的物品必须来自某个地方。 从哪里来?
在D中,我们可以使用static if
并根据不同条件定义类的不同部分。 在某些Ruby中,我们可以使用include方法将方法混合到类中 。 但是我们在C ++中,到目前为止,我们的可能性非常有限:我们可以直接在类内部定义方法/属性,也可以从某些基类继承该方法/属性。
我们不能根据某些条件在类内定义不同的方法/属性,因为 if constexpr
不是D static if
C ++ static if
。 因此,仅保留继承。
更新。 正如评论中建议的那样,我在这里应该更仔细地讲。 由于C ++具有SFINAE,因此我们可以通过SFINAE启用/禁用类中各个方法的可见性(即达到类似于static if
的效果)。 但是我认为这种方法有两个严重的缺点。 首先,如果这些方法不是1-2-3,而是4-5或更多,那么使用SFINAE格式化每个方法就很麻烦,这会影响代码的可读性。 其次,SFINAE无法帮助我们添加/删除类属性(字段)。
在C ++中,我们可以定义几个基类,然后从这些基类继承message_holder_t
。 并且已经使用std :: conditional根据模板参数的值来选择一个或另一个基类。
但是诀窍在于,我们不仅需要一组基类,还需要一小部分继承链。 在开始时,将有一个类来确定在任何情况下都将需要的常规功能。 接下来是将确定“智能指针”行为逻辑的基类。 然后将有一个确定必要的获取方法的类。 按此顺序,我们将考虑实现的类。
SObjectizer已经具有现成的模板魔术来简化我们的任务,该魔术可以确定是否从message_t继承消息 ,以及检查消息可变性的方法 。 因此,在实现中,我们将仅使用此现成的魔术,而不会深入研究其工作细节。
通用指针存储库
让我们从存储相应的intrusive_ptr的通用基本类型开始,还提供任何message_holder_t
实现所需的通用方法集:
template< typename Payload, typename Envelope > class basic_message_holder_impl_t { protected : intrusive_ptr_t< Envelope > m_msg; public : using payload_type = Payload; using envelope_type = Envelope; basic_message_holder_impl_t() noexcept = default; basic_message_holder_impl_t( intrusive_ptr_t< Envelope > msg ) noexcept : m_msg{ std::move(msg) } {} void reset() noexcept { m_msg.reset(); } [[nodiscard]] bool empty() const noexcept { return static_cast<bool>( m_msg ); } [[nodiscard]] operator bool() const noexcept { return !this->empty(); } [[nodiscard]] bool operator!() const noexcept { return this->empty(); } };
该模板类具有两个参数。 第一个,有效载荷,设置吸气方法应使用的类型。 而第二个Envelope设置intrusive_ptr的类型。 如果消息类型是从message_t
继承的message_t
这两个参数将具有相同的值。 但是,如果消息不是从message_t
继承的,则消息类型将用作有效负载,而user_type_message_t<Payload>
将user_type_message_t<Payload>
信封。
我认为基本上该课程的内容不会引起任何疑问。 但是,应分别注意两件事。
首先,指针本身,即 m_msg属性在protected部分中定义,以便类继承者可以访问它。
其次,对于此类,编译器本身会生成所有必需的构造函数和复制/移动运算符。 在这个级别的水平上,我们尚未禁止任何操作。
shared_ptr和unique_ptr行为的单独基础
因此,我们有一个存储指向消息的指针的类。 现在,我们可以定义其继承人,其继承人将表现为shared_ptr或unique_ptr。
让我们从shared_ptr行为的情况开始,因为 这是最少的代码:
template< typename Payload, typename Envelope > class shared_message_holder_impl_t : public basic_message_holder_impl_t<Payload, Envelope> { using direct_base_type = basic_message_holder_impl_t<Payload, Envelope>; public : using direct_base_type::direct_base_type; [[nodiscard]] intrusive_ptr_t< Envelope > make_reference() const noexcept { return this->m_msg; } };
没什么复杂的:从basic_message_holder_impl_t
继承,继承其所有构造函数,并定义make_reference()
的简单,无损实现。
对于unique_ptr行为,代码虽然没有什么复杂之处,但是代码更大:
template< typename Payload, typename Envelope > class unique_message_holder_impl_t : public basic_message_holder_impl_t<Payload, Envelope> { using direct_base_type = basic_message_holder_impl_t<Payload, Envelope>; public : using direct_base_type::direct_base_type; unique_message_holder_impl_t( const unique_message_holder_impl_t & ) = delete; unique_message_holder_impl_t( unique_message_holder_impl_t && ) = default; unique_message_holder_impl_t & operator=( const unique_message_holder_impl_t & ) = delete; unique_message_holder_impl_t & operator=( unique_message_holder_impl_t && ) = default; [[nodiscard]] intrusive_ptr_t< Envelope > make_reference() noexcept { return { std::move(this->m_msg) }; } };
同样,我们从basic_message_holder_impl_t
继承并basic_message_holder_impl_t
继承所需的构造函数(这是默认构造函数和初始化构造函数)。 但同时,我们根据unique_ptr逻辑定义构造函数和复制/移动运算符:我们禁止复制,而是实现移动。
我们make_reference()
还有一个破坏性的make_reference()
方法。
实际上,仅此而已。 仍然只有实现这两个基类之间的选择...
在shared_ptr和unique_ptr行为之间进行选择
要在shared_ptr和unique_ptr行为之间进行选择,您需要以下元函数(元函数,因为它“在编译时”与类型配合使用):
template< typename Msg, message_ownership_t Ownership > struct impl_selector { static_assert( !is_signal<Msg>::value, "Signals can't be used with message_holder" ); using P = typename message_payload_type< Msg >::payload_type; using E = typename message_payload_type< Msg >::envelope_type; using type = std::conditional_t< message_ownership_t::autodetected == Ownership, std::conditional_t< message_mutability_t::immutable_message == message_mutability_traits<Msg>::mutability, shared_message_holder_impl_t<P, E>, unique_message_holder_impl_t<P, E> >, std::conditional_t< message_ownership_t::shared == Ownership, shared_message_holder_impl_t<P, E>, unique_message_holder_impl_t<P, E> > >; };
此元函数接受message_holder_t
参数列表中的两个参数,结果(即,嵌套type
的定义)“返回”应从其继承的类型。 即 shared_message_holder_impl_t
或unique_message_holder_impl_t
。
在impl_selector
的定义内impl_selector
您可以看到上面提到的魔术痕迹,而我们没有涉及到它: message_payload_type<Msg>::payload_type
, message_payload_type<Msg>::envelope_type
message_mutability_traits<Msg>::mutability
和message_mutability_traits<Msg>::mutability
。
为了更轻松地使用impl_selector
,我们将为其定义一个较短的名称:
template< typename Msg, message_ownership_t Ownership > using impl_selector_t = typename impl_selector<Msg, Ownership>::type;
吸气剂的基础
因此,我们已经有机会选择一个包含指针并定义“智能指针”行为的基础。 现在,我们需要为该基础提供getter方法。 为什么我们需要一个简单的类:
template< typename Base, typename Return_Type > class msg_accessors_t : public Base { public : using Base::Base; [[nodiscard]] Return_Type * get() const noexcept { return get_ptr( this->m_msg ); } [[nodiscard]] Return_Type & operator * () const noexcept { return *get(); } [[nodiscard]] Return_Type * operator->() const noexcept { return get(); } };
这是一个依赖两个参数的模板类,但是它们的含义完全不同。 基本参数将是上面显示的impl_selector
元函数的结果。 即 作为Base参数,设置要继承的基类。
重要的是要注意,如果继承来自unique_message_holder_impl_t
,禁止使用该构造函数和复制运算符,则编译器将无法生成msg_accessors_t
的构造函数和复制运算符。 这就是我们所需要的。
return_Type参数将是消息的类型,getter将返回消息的指针/链接。 诀窍是,对于类型为Msg
的不可变消息Msg
Return_Type参数将设置为const Msg
。 对于类型为Msg
的可变消息Msg
参数Return_Type将具有值Msg
。 因此, get()
方法将为不变消息返回const Msg*
,而对于可变消息仅返回Msg*
。
使用免费函数get_ptr()
解决了使用未从message_t
继承的消息的问题:
template< typename M > M * get_ptr( const intrusive_ptr_t<M> & msg ) noexcept { return msg.get(); } template< typename M > M * get_ptr( const intrusive_ptr_t< user_type_message_t<M> > & msg ) noexcept { return std::addressof(msg->m_payload); }
即 如果消息不是从message_t
继承并存储为user_type_message_t<Msg>
,则调用第二个重载。 而且如果它是继承的,那么第一个重载。
为吸气剂选择特定的基础
因此, msg_accessors_t
模板需要两个参数。 第一个由impl_selector
元函数计算。 但是为了从msg_accessors_t
形成特定的基本类型,我们需要确定第二个参数的值。 为此,还需要一个元功能:
template< message_mutability_t Mutability, typename Base > struct accessor_selector { using type = std::conditional_t< message_mutability_t::immutable_message == Mutability, msg_accessors_t<Base, typename Base::payload_type const>, msg_accessors_t<Base, typename Base::payload_type> >; };
您只能注意Return_Type参数的计算。 East const有用的少数情况之一;)
好吧,为了提高以下代码的可读性,可以使用一个更紧凑的选项:
template< message_mutability_t Mutability, typename Base > using accessor_selector_t = typename accessor_selector<Mutability, Base>::type;
最后的后继message_holder_t
现在,您可以查看message_holder_t
什么,对于实现所有这些基本类和元功能都是必需的(从实现中删除了构造存储在message_holder中的消息实例的方法的一部分):
template< typename Msg, message_ownership_t Ownership = message_ownership_t::autodetected > class message_holder_t : public details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> > { using base_type = details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> >; public : using payload_type = typename base_type::payload_type; using envelope_type = typename base_type::envelope_type; using base_type::base_type; friend void swap( message_holder_t & a, message_holder_t & b ) noexcept { using std::swap; swap( a.message_reference(), b.message_reference() ); } };
实际上,我们上面分析的所有内容都是必需的,以便记录以下两个元函数的“调用”:
details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> >
因为 这不是第一种选择,而是简化和减少代码的结果,我可以说紧凑的元函数形式可以大大减少代码量并提高其可理解性(如果通常在这里谈论可理解性)。
如果...
但是,如果在C ++中if constexpr
和在D中一样强大,那么您可以编写如下代码:
假设版本,如果使用constexpr,则具有更高级的功能 template< typename Msg, message_ownership_t Ownership = message_ownership_t::autodetected > class message_holder_t { static constexpr const message_mutability_t Mutability = details::message_mutability_traits<Msg>::mutability; static constexpr const message_ownership_t Actual_Ownership = (message_ownership_t::unique == Ownership || (message_mutability_t::mutable_msg == Mutability && message_ownership_t::autodetected == Ownership)) ? message_ownership_t::unique : message_ownership_t::shared; public : using payload_type = typename message_payload_type< Msg >::payload_type; using envelope_type = typename message_payload_type< Msg >::envelope_type; private : using getter_return_type = std::conditional_t< message_mutability_t::immutable_msg == Mutability, payload_type const, payload_type >; public : message_holder_t() noexcept = default; message_holder_t( intrusive_ptr_t< envelope_type > mf ) noexcept : m_msg{ std::move(mf) } {} if constexpr(message_ownership_t::unique == Actual_Ownership ) { message_holder_t( const message_holder_t & ) = delete; message_holder_t( message_holder_t && ) noexcept = default; message_holder_t & operator=( const message_holder_t & ) = delete; message_holder_t & operator=( message_holder_t && ) noexcept = default; } friend void swap( message_holder_t & a, message_holder_t & b ) noexcept { using std::swap; swap( a.m_msg, b.m_msg ); } [[nodiscard]] getter_return_type * get() const noexcept { return get_const_ptr( m_msg ); } [[nodiscard]] getter_return_type & operator * () const noexcept { return *get(); } [[nodiscard]] getter_return_type * operator->() const noexcept { return get(); } if constexpr(message_ownership_t::shared == Actual_Ownership) { [[nodiscard]] intrusive_ptr_t< envelope_type > make_reference() const noexcept { return m_msg; } } else { [[nodiscard]] intrusive_ptr_t< envelope_type > make_reference() noexcept { return { std::move(m_msg) }; } } private : intrusive_ptr_t< envelope_type > m_msg; };
对我来说,差异太惊人了。而且它们也不赞成当前的C ++:(
(上面以一个连续的“ footfoot”形式分解的C ++代码可以在此处看到)。
顺便说一句,我并没有真正了解C ++未来版本的元编程和反射建议领域中正在发生的事情。但是从我的记忆中,我会感觉到Sutter提出的元类不会大大简化这一特定任务。据我了解,借助元类,可以编写一个类生成器message_holder_t
。这样的生成器可能写起来很简单,但是在这种特定情况下,这种方法不太可能比真正高级的方法更具表达性和可理解性if constexpr
。
结论
, C++. , . , , .
, .
, , ++ , . , . , , . , . C++98/03 , C++11 .