... how to fill the template class with different contents depending on the values of the template parameters?
Once, quite a long time ago, the D language was started to be done as “correct C ++”, taking into account the experience accumulated in C ++. Over time, D became a no less complex and more expressive language than C ++. And already C ++ began to spy on D. For example, appearing in C ++ 17 if constexpr
, in my opinion, is a direct borrowing from D, the prototype of which was D-shny static if .
To my regret, if constexpr
in C ++ does not have the same power as static if
in D. There are some reasons for this , but there are still cases where you can only regret that if constexpr
in C ++ does not allow you to control the content of C + + class. I want to talk about one of these cases.
It will be about how to make a template class, the contents of which (that is, the composition of the methods and the logic of some of the methods) would change depending on which parameters were passed to this template class. The example is taken from real life, from the experience of developing a new version of SObjectizer .
You need to create a clever version of the smart pointer to store message objects. So that you can write something like:
message_holder_t<my_message> msg{ new my_message{...} }; send(target, msg); send(another_target, msg);
The trick of this class is message_holder_t
in that you need to take into account three important factors.
The types of messages that are parameterized by message_holder_t
are divided into two groups. The first group is messages that are inherited from the special basic type message_t
. For example:
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} {} };
In this case, the message_holder_t inside itself must contain only a pointer to an object of this type. The same pointer should be returned in getter methods. Ie, for the case of a successor from message_t
should be something like:
template<typename M> class message_holder_t { intrusive_ptr_t<M> m_msg; public: ... const M * get() const noexcept { return m_msg.get(); } };
The second group is messages of arbitrary user types that are not inherited from message_t
. For example:
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} {} };
Instances of such types in SObjectizer are not sent by themselves, but by prisoners in a special wrapper for user_type_message_t<M>
, which is already inherited from message_t
. Therefore, for such types, message_holder_t
must contain a pointer to user_type_message_t<M>
inside, and getter methods must return a pointer to 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); } };
The second factor is the division of messages into immutable and mutable. If the message is immutable (and by default it is immutable), then getter methods should return a constant pointer to the message. And if modifiable, then getters should return a non-constant pointer. Those. there must be something like:
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.
The third factor is the logic of the behavior of message_holder_t
as a smart pointer. Sometime it should behave like std::shared_ptr
, i.e. You can have multiple message_holders that refer to the same message instance. And once it should behave like std::unique_ptr
, i.e. only one message_holder instance can refer to the message instance.
By default, the behavior of message_holder_t
should depend on the variability / immutability of the message. Those. with immutable messages, message_holder_t
should behave like std::shared_ptr
, and with mutable ones, like std::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.
But life is difficult, so you need to have the opportunity to manually set the message_holder_t
behavior. So that you can make a message_holder for an immutable message that behaves like unique_ptr. And so that you can make a message_holder for a changeable message that behaves like 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.
Accordingly, when message_holder_t
works as shared_ptr, it should have the usual set of constructors and assignment operators: both copy and move. In addition, there must be a constant make_reference
method that returns a copy of the pointer stored inside message_holder_t
.
But when message_holder_t
works as unique_ptr, then its constructor and copy operator should be prohibited. And the make_reference
method should remove the pointer from the message_holder_t
object: after the make_reference
call make_reference
original message_holder_t
should remain empty.
So, you need to create a template class:
template< typename M, message_ownership_t Ownership = message_ownership_t::autodetected> class message_holder_t {...};
which one:
intrusive_ptr_t<M>
or intrusive_ptr<user_type_message_t<M>>
depending on whether M is inherited from message_t
;const M*
or M*
depending on the variability / immutability of the message;make_reference()
method must either return a copy of the stored intrusive_ptr, or it must remove the intrusive_ptr value and leave the original message_holder_t
in an empty state. In the first case, make_reference()
should be constant, in the second - non-constant method.The last two items from the list are determined by the Ownership parameter (as well as the message mutability, if the autodetected
value is used for Ownership).
In this section, we consider all the components from which the final solution is obtained. Well, the very resultant decision. The code snippets that are cleared of all distracting details will be shown. If someone is interested in the real code, then you can see it here .
The solution shown below does not claim beauty, ideality or a role model. It was found, implemented, tested and documented in a short time, under the pressure of time. Perhaps, if there was more time, and the search for a solution was more young, An intelligent and well-versed developer in modern C ++, it would have turned out to be more compact, simpler and more understandable. But, as it turned out, it turned out well ... "Don't shoot the pianist", in general.
So, we need to have a class with several sets of methods. The contents of these sets should come from somewhere. From where
In the D language, we could use static if
and define different parts of a class depending on different conditions. In some Ruby, we could add methods to our class using the include method . But we are in C ++, in which so far our possibilities are very limited: we can either define the method / attribute directly inside the class, or we can inherit the method / attribute from some base class.
We cannot define different methods / attributes within the class depending on some condition, since C ++ if constexpr
is not a D-shny static if
. Consequently, only inheritance remains.
Upd. As I was told in the comments, you should speak more carefully here. Since in C ++ there is SFINAE, we can turn on / off the visibility of individual methods in a class (i.e., achieve an effect similar to static if
) with SFINAE. But this approach has two serious, in my opinion, flaws. First, if such methods are not 1-2-3, but 4-5 or more, then it is tiring to draw each of them through SFINAE, and it affects the readability of the code. Secondly, SFINAE does not help us add / remove class attributes (fields).
In C ++, we can define several base classes, from which we then inherit message_holder_t
. And the choice of one or another base class will be done, depending on the values of the template parameters, by means of std :: conditional .
But the trick is that we need not just a set of base classes, but a small chain of inheritance. At the beginning of it will be a class that will define the overall functionality that is required in any case. Next will be the base classes that will determine the logic of the smart pointer behavior. And then there will be a class that will determine the necessary getters. In this order, we consider the implemented classes.
Our task is simplified by the fact that in SObjectizer there is already ready template magic, which determines whether the message is inherited from message_t , as well as the means for checking the message mutability . Therefore, in the implementation we will simply use this ready-made magic and will not dive into the details of its work.
Let's start with a common base type that stores the corresponding intrusive_ptr, and also provides the common set of methods that any of the message_holder_t
implementations needs:
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(); } };
This template class has two parameters. The first, Payload, specifies the type that getter methods should use. While the second, Envelope, sets the type for intrusive_ptr. In the case when the message type is inherited from message_t
both of these parameters will have the same value. But if the message is not inherited from message_t
, then the message type will be used as the Payload, and user_type_message_t<Payload>
will act as the Envelope.
I think that basically the contents of this class raises no questions. But separately should pay attention to two things.
First, the pointer itself, i.e. The m_msg attribute is defined in the protected section so that classes can have access to it.
Secondly, for this class, the compiler itself generates all the necessary constructors and copy / move operators. And at the level of this class, we do not prohibit anything yet.
So, we have a class that stores a pointer to a message. Now we can define its successors, which will behave either as shared_ptr or as unique_ptr.
Let's start with the case of shared_ptr-behavior, since here is the least code:
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; } };
Nothing complicated: inherit from basic_message_holder_impl_t
, inherit all of its constructors, and define a simple, non-destructive implementation of make_reference()
.
For the case of unique_ptr-behavior, the code is bigger, although there is nothing complicated in it:
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) }; } };
Again, we inherit from basic_message_holder_impl_t
and inherit the constructors we need from it (this is the default constructor and initialization constructor). But, at the same time, we define constructors and copy / move operators in accordance with the unique_ptr logic: copying is prohibited, we implement movement.
We also have a destructive make_reference()
method make_reference()
.
That's all. It remains only to realize the choice between these two base classes ...
To choose between shared_ptr- and unique_ptr-behavior, the following meta-function is required (it’s a meta-function because it “works” with types in compile-time):
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> > >; };
This metafunction takes both parameters from the message_holder_t
parameter list and as a result (i.e., the nested type definition) “returns” the type from which to be inherited. Those. either shared_message_holder_impl_t
, or unique_message_holder_impl_t
.
Inside the definition of impl_selector
you can see traces of the magic mentioned above, which we didn’t go into: message_payload_type<Msg>::payload_type
, message_payload_type<Msg>::envelope_type
and message_mutability_traits<Msg>::mutability
.
And in order to use the impl_selector
metafunction impl_selector
was easier, next we define a shorter name for it:
template< typename Msg, message_ownership_t Ownership > using impl_selector_t = typename impl_selector<Msg, Ownership>::type;
So, we already have the opportunity to choose a database that contains a pointer and defines the behavior of a smart pointer. Now you need to provide this database with getter methods. For which we need one simple class:
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(); } };
This is a template class that depends on two parameters, but their meaning is completely different. The Base parameter will be the result of the impl_selector
shown above. Those. Base parameter is the base class from which you want to inherit.
It is important to note that if inheritance comes from unique_message_holder_impl_t
, for which the constructor and the copy operator are prohibited, the compiler will not be able to generate the constructor and the copy operator for msg_accessors_t
. What we need.
The Return_Type parameter will be the type of message, the pointer / link to which will be returned by getters. The trick is that for an immutable message of type Msg
the Return_Type parameter will have the value const Msg
. Whereas for a Msg
mutable message, the Return_Type parameter will be Msg
. Thus, the get()
method will return const Msg*
for immutable messages, and simply Msg*
for mutable messages.
Using the free function get_ptr()
problem of working with messages that are not inherited from message_t
solved:
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); }
Those. if the message is not inherited from message_t
and stored as user_type_message_t<Msg>
, then the second overload is called. And if inherited, then the first overload.
So, the msg_accessors_t
template requires two parameters. The first is calculated by the impl_selector
. But in order to form a specific base type of msg_accessors_t
, we need to determine the value of the second parameter. For this is another meta-function:
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> >; };
You can only pay attention to the calculation of the Return_Type parameter. One of those few cases when east const is useful;)
Well and, for increase of readability of the subsequent code, more compact variant for work with it:
template< message_mutability_t Mutability, typename Base > using accessor_selector_t = typename accessor_selector<Mutability, Base>::type;
Now you can look at what message_holder_t
, for the implementation of which all these base classes and metafunctions were required (some of the methods for constructing an instance of messages stored in message_holder were removed from the implementation):
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() ); } };
Essentially, everything that we discussed above was required in order to record this “challenge” of two metafunctions:
details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> >
Because this is not the first option, but the result of simplifying and shortening the code, I can say that compact forms of metafunctions very well reduce the amount of code and increase its comprehensibility (if it is appropriate to speak about comprehensibility here).
But if in C ++ if constexpr
was as powerful as static if
in D, then you could write something like:
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; };
, , ++. , , , . , message_holder_t
. , , if constexpr
.
, C++. , . , , .
, .
, , ++ , . , . , , . , . C++98/03 , C++11 .
Source: https://habr.com/ru/post/449122/
All Articles