遗憾的是,在C ++中缺少一个成熟的static if或...

...如何根据模板参数的值用不同的内容填充模板类?


很久以来,考虑到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{...}; //  . const int a = msg1->a_; // OK. msg1->a_ = 0; //     ! message_holder_t<mutable_msg<user_message>> msg2{...}; //  . const int a = msg2->a_; // OK. msg2->a_ = 0; // OK. 

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_ptrstd::unique_ptr


 message_holder_t<so5_message> msg1{...}; message_holder_t<so5_message> msg2 = msg; // OK. message_holder_t<mutable_msg<user_message>> msg3{...}; message_holder_t<mutable_msg<user_message>> msg4 = msg3; // !  ! message_holder_t<mutable_msg<user_message>> msg5 = std::move(msg3); // OK. 

但是生活是一件复杂的事情,因此您还需要能够手动设置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; // !  ! unique_so5_message msg3 = std::move(msg); // OK,   msg3. using shared_user_messsage = so_5::message_holder_t< so_5::mutable_msg<user_message>, so_5::message_ownership_t::shared>; shared_user_message msg4{...}; shared_user_message msg5 = msg4; // OK. 

因此,当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_tunique_message_holder_impl_t


impl_selector的定义内impl_selector您可以看到上面提到的魔术痕迹,而我们没有涉及到它: message_payload_type<Msg>::payload_typemessage_payload_type<Msg>::envelope_type message_mutability_traits<Msg>::mutabilitymessage_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 .

Source: https://habr.com/ru/post/zh-CN449122/


All Articles