В языке C++ мне очень не хватает т.н. strong typedef. Временами хочется сказать, что вот этот вот int -- это ширина (width), а вот этот вот int -- это высота (height). Но в C++ штатных возможностей вывести разные несовместимые друг с другом типы из одного базового нет. Поэтому приходится велосипедить. Иногда просто вот так:
struct width_t { int value; } struct height_t { int height; } void fill(width_t w, height_t h) {...} fill(width_t{640}, height_t{480}); |
Иногда и посложнее.
Для упрощения себе жизни когда-то сделал несложный вспомогательный тип tagged_value_t по типу вот такого:
template< typename V, typename Tag > class tagged_value_t { V m_value; public: explicit tagged_value_t( const V & value ) : m_value( value ) {} explicit tagged_value_t( V && value ) : m_value( std::forward<V>(value) ) {} [[nodiscard]] const V & value() const noexcept { return m_value; } [[nodiscard]] V & value() noexcept { return m_value; } }; |
Этот тип кочует у меня из одного проекта в другой и позволяет делать что-то вроде:
struct url_tag {}; using url_t = tagged_value_t< std::string, url_tag >; struct username_tag {}; using username_t = tagged_value_t< std::string, username_tag >; struct raw_password_tag {}; using raw_password_t = tagged_value_t< std::string, raw_password_tag >; struct base64_password_tag {}; using base64_password_tag = tagged_value_t< std::string, base64_password_tag >; |
Подобные штуки применяю для указания типов параметров для функций/методов/конструкторов. В принципе удобно: на входе в функцию/метод ошибиться сложно, а внутри можно извлекать значения исходных типов (тех же int или std::string) и работать с ними привычными способами.
Но вот давеча захотелось сделать еще и так, чтобы значения, обернутые в tagged_value, можно было хранить в качестве ключей в ассоциативных контейнерах. Например, в качестве ключа для std::map.
Понятное дело, что для каждого tagged_value выписывать ручками operator< или operator== не хочется. Поэтому решил попробовать посредством метапрограммирования модифицировать tagged_value_t так, чтобы в нем появлялись operator< и/или operator== если таковые операторы определены для исходного типа.
Под катом то, что у меня получилось в первом приближении. Для C++17, т.к. это самый "свежий" стандарт, который я могу себе позволить. Возможно, концепты из C++20 позволят записать все это компактнее.
Получилось довольно объемно и не сказать, чтобы тривиально.
Хотя здесь я должен сделать важный дисклаймер: мои знания C++ за последние года 2-3 заметно ухудшились, т.к. особо упражняться было негде, текущая загрузка не предусматривает необходимости углубляться в тонкости языка или строить какие-то сложные шаблонные конструкции. Так что прошу принять и простить, пианист играет как умеет... :(
Не буду вдаваться сильно в детали, т.к. есть ощущение, что был выбран вообще неправильный подход. Вместо того, чтобы tagged_value_t сам выводил наличие операторов у исходного типа, нужно бы явно указывать какие именно операторы мне нужны. Но это будет уже следующим шагом в экспериментах. Пока же то, что есть на данный момент.
Основная же идея такова (на примере operator==, т.к. он тривиальнее): нужно иметь два базовых класса, один из которых реализует operator==, а второй нет. Что-то вроде:
template< typename Derived > struct equal_to_operator_impl_t { private: [[nodiscard]] const Derived & as_derived() const noexcept { return static_cast<const Derived &>(*this); } public: [[nodiscard]] bool operator==( const Derived & o ) const noexcept( noexcept(as_derived().value() == o.value()) ) { return as_derived().value() == o.value(); } }; struct no_equal_to_operator_impl_t { }; |
Эти типы предназначены для того, чтобы их использовали в качестве базовых типов в паттерне CRTP. Соответственно, нужна метафункция-селектор, которая выберет один из этих типов:
template< typename Tagged_Value, bool Has_Equal_To_Comparison > struct equal_to_operator_provider_t_fn { using type = no_equal_to_operator_impl_t; }; template< typename Tagged_Value > struct equal_to_operator_provider_t_fn< Tagged_Value, true > { using type = equal_to_operator_impl_t< Tagged_Value >; }; template< typename Value, typename Tagged_Value > using equal_to_operator_selector_t = typename equal_to_operator_provider_t_fn< Tagged_Value, has_equal_to_comparison_v_fn< Value >::value >::type; |
В роли метафункции-селектора выступает equal_to_operator_selector_t, а он, в свою очередь, опирается на equal_to_operator_provider_t_fn. Причем шаблон equal_to_operator_provider_t_fn через частичную специализацию имеет две версии: одну для случая, когда operator== есть (тогда Has_Equal_To_Comparison равен true), а вторую -- когда operator== нет.
Соответственно, задача в том, чтобы правильно вычислить значение параметра Has_Equal_To_Comparison. И для этого нужна еще одна метафункция:
template< typename, typename = std::void_t<> > struct has_equal_to_comparison_v_fn : public std::false_type {}; template< typename T > struct has_equal_to_comparison_v_fn< T, std::void_t< decltype( std::declval<T>() == std::declval<T>() ) > > : public std::true_type {}; |
Вот, в общем-то и все. Остается только задействовать метафункцию-селектор в списке базовых типов для tagged_value_t.
Не скажу, что написать все это было сложно или долго, т.к. основную часть шаблонной магии я подсмотрел в потрохах нашей json_dto, хотя местами почесать репу пришлось. Ну да, объемно, блин, получилось :( Тестирование самое минимальное, так что на каких-то граничных случаях может не работать от слова совсем.
И не обошлось без забавных сочетаний символов, например: std::less<>{}(). Почувствуй себя растоманом, что называется :)
В очередной раз остается пожалеть, что в C++ нет static if из D, реализация tagged_value_t с таким static if была бы гораздо проще и компактнее.
Отдельно доставило дублирование кода для того, чтобы вывести noexcept для операторов сравнения/равенства. Насколько я понимаю, здесь C++ ничего не может предложить.
Если кто-то будет разбираться с кодом, то для operator< я специально заморочился на использование std::less, т.к. вроде бы std::less позволяет безопасно сравнивать указатели в C++ (что, вообще-то говоря, опасно делать через встроенный оператор сравнения для указателей, т.к. это чревато UB).
В общем, вот код, а поиграться с ним можно на wandbox-е.
Если кого-то еще интересует эта тема, то что интересное я когда-то видел в библиотеке type_safe от foonathan (документация по strong_typedef).
#include <iostream> #include <set> #include <string> #include <functional> #include <utility> #include <type_traits> namespace tagged_value_impl { template< typename, typename = std::void_t<> > struct has_less_then_comparison_v_fn : public std::false_type {}; template< typename T > struct has_less_then_comparison_v_fn< T, std::void_t< decltype( std::declval<T>() < std::declval<T>() ) > > : public std::true_type {}; template< typename Derived > struct less_then_operator_impl_t { private: [[nodiscard]] const Derived & as_derived() const noexcept { return static_cast<const Derived &>(*this); } public: [[nodiscard]] bool operator<( const Derived & o ) const noexcept( noexcept( std::less<>{}( as_derived().value(), o.value() )) ) { return std::less<>{}( as_derived().value(), o.value() ); } }; struct no_less_then_operator_impl_t { }; template< typename Tagged_Value, bool Has_Less_Then_Comparison > struct less_then_operator_provider_t_fn { using type = no_less_then_operator_impl_t; }; template< typename Tagged_Value > struct less_then_operator_provider_t_fn< Tagged_Value, true > { using type = less_then_operator_impl_t< Tagged_Value >; }; template< typename Value, typename Tagged_Value > using less_then_operator_selector_t = typename less_then_operator_provider_t_fn< Tagged_Value, has_less_then_comparison_v_fn< Value >::value >::type; template< typename, typename = std::void_t<> > struct has_equal_to_comparison_v_fn : public std::false_type {}; template< typename T > struct has_equal_to_comparison_v_fn< T, std::void_t< decltype( std::declval<T>() == std::declval<T>() ) > > : public std::true_type {}; template< typename Derived > struct equal_to_operator_impl_t { private: [[nodiscard]] const Derived & as_derived() const noexcept { return static_cast<const Derived &>(*this); } public: [[nodiscard]] bool operator==( const Derived & o ) const noexcept( noexcept(as_derived().value() == o.value()) ) { return as_derived().value() == o.value(); } }; struct no_equal_to_operator_impl_t { }; template< typename Tagged_Value, bool Has_Equal_To_Comparison > struct equal_to_operator_provider_t_fn { using type = no_equal_to_operator_impl_t; }; template< typename Tagged_Value > struct equal_to_operator_provider_t_fn< Tagged_Value, true > { using type = equal_to_operator_impl_t< Tagged_Value >; }; template< typename Value, typename Tagged_Value > using equal_to_operator_selector_t = typename equal_to_operator_provider_t_fn< Tagged_Value, has_equal_to_comparison_v_fn< Value >::value >::type; } /* namespace tagged_value_impl */ // // tagged_value_t // template< typename V, typename Tag > class tagged_value_t : public tagged_value_impl::less_then_operator_selector_t< V, tagged_value_t< V, Tag > > , public tagged_value_impl::equal_to_operator_selector_t< V, tagged_value_t< V, Tag > > { V m_value; public: using value_type = V; explicit tagged_value_t( const V & value ) : m_value( value ) {} explicit tagged_value_t( V && value ) : m_value( std::forward<V>(value) ) {} template< typename... Args > tagged_value_t( std::piecewise_construct_t /*not_used*/, Args && ...args ) : m_value{ std::forward<Args>(args)... } {} [[nodiscard]] const V & value() const noexcept { return m_value; } [[nodiscard]] V & value() noexcept { return m_value; } }; using namespace std::string_literals; struct ip_tag {}; using ip_t = tagged_value_t< std::string, ip_tag >; struct simple_values_tag {}; using simple_ptr_t = tagged_value_t< int*, simple_values_tag >; struct my_data { std::string m_data; }; struct binary_tag {}; using my_binaries_t = tagged_value_t< my_data, binary_tag >; int main() { ip_t a{ "192.168.1.1" }; ip_t b{ "128.0.0.1" }; std::cout << "a < b: " << (a < b) << std::endl; std::cout << "a > b: " << (b < a) << std::endl; std::cout << "a == b: " << (a == b) << std::endl; std::cout << "b == a: " << (b == a) << std::endl; std::cout << "a == a: " << (a == a) << std::endl; std::cout << "b == b: " << (b == b) << std::endl; std::set< ip_t > ips{ ip_t{ "192.168.1.1" }, ip_t{ "128.0.0.1" }, ip_t{ "10.1.1.1" }, { std::piecewise_construct, "192.168.100.1" }, { std::piecewise_construct, "192.168.1.100" } }; for( const auto & ip : ips ) std::cout << "ip: " << ip.value() << std::endl; int i1 = 0; char padding_1 = 0; (void)padding_1; int i2 = 3; char padding_2 = 0; (void)padding_2; int i3 = 4; std::set< simple_ptr_t > ints{ simple_ptr_t{ &i2 }, simple_ptr_t{ &i3 }, simple_ptr_t{ &i1 } }; std::cout << &i1 << " " << &i2 << " " << &i3 << std::endl; for( const auto & p : ints ) std::cout << p.value() << " "; std::cout << std::endl; #if 0 my_binaries_t bin1{ my_data{ "123456"s } }; my_binaries_t bin2{ my_data{ "567890"s } }; const auto cr = (bin1 < bin2); const auto er = (bin1 == bin2); #endif } |
7 комментариев:
на взгляд прикладника вполне годное и читаемое решение. void_t стала стандартной идиомой.
я для краткости использую декларацию вместо определения:
using url_t = tagged_value_t< std::string, struct url_tag >;
Если задавать поддерживаемые операторы явно - получится boost.operators.
@sergegers
Кстати да, спасибо, что напомнили.
Но у меня была идея записывать как-то так:
using url_t = tagged_value<std::string, url_tag, std::less, std::equal_to>
Т.е. шаблон был бы какой-то такой:
template<typename V, typename Tag, typename... Operators>
class tagged_value_t : ...
И список необходимых базовых классов для tagged_value_t формировался бы на основе разбора шаблонного параметра Operators.
Но вот, боюсь, там будет такая шаблонная магия, что сопровождать это будет скучно. Да и на времени компиляции это наверняка будет сказываться не лучшим образом.
@Alex
На тему объявления struct url_tag прямо по месту недавно была хорошая статья https://habr.com/ru/company/pvs-studio/blog/715436/
@eao197 У меня на boost.operators сделан самодельный strong_typedef. Сделаны несколько шаблонов, поддерживающие разные наборы операций. Их можно использовать как вы предполагаете - с помошью using декларации с базовым типом, тегом типа и дефолтным значением или с наследованием, где можно добавить дополнительные операции, методы и интероперабельность с другими типами.
@eao197, вариант со свободной функцией без наследования не рассматривали?
template
auto operator < (const tagged_value_t& v1, const tagged_value_t& v2) -> decltype(v1.value() < v2.value())
{
return v1.value() < v2.value();
}
@Константин
Нет, не рассматривал. Нужно будет подумать об этом.
Но сейчас приоритеты сместились, есть ощущение, что получится вернуться к этим экспериментам не раньше чем через неделю (если это вообще останется актуальным).
Отправить комментарий