Представляю сообществу библиотеку feature из состава разрабатываемых мной библиотек под общим названием ScL. Сам набор библиотек ScL систематизирует достаточно скромный набор реализаций и подходов, которые на мой взгляд могут упростить процесс разработки программного обеспечения на С++.
Инструменты библиотеки feature позволяют наделить экземпляры объектов любого типа свойствами, которых у них изначально не существует. К таким свойствам можно отнести, например, ленивые вычисления (implicit shared и другое), потокобезопасность, выбор способа размещения объекта "по месту" или в "куче" и т.д.
Все перечисленные свойства добавляются с помощью реализации механизма "умных" оберток, базовый набор которых представлен в библиотеке feature и может быть легко расширен любыми пользовательскими решениями.
void foo ()
{
using namespace ::ScL::Feature;
using Tool = Implicit::Shared;
using Text = Wrapper< ::std::string, Tool >;
Text text = "Hello World!";
Text other = text; // implicit shared "Hello World!"
}
Хотите узнать как? Прошу под кат.
Мотивация
Не возникает ли у вас чувство досады, если готовое решение не может быть использовано в вашем приложении из-за того, что не обладает необходимыми свойствами? И приходится формировать громоздкую, часто однотипную дополнительную функциональность поверх того, что есть. Или, что ещё чаще, писать собственную реализацию очередного велосипеда, отличающегося цветом, рамой или формой колес.
Вот некоторые случаи желательной дополнительной функциональности и примеры их реализации.
Strong typedef
В программах часто один и тот же тип используется для декларации совершенно не совместимых между собой понятий. Например, типом std::string могут быть представлены url, e-mail, ФИО, адрес и т.д. И что делать, если для каждого из этих типов предусмотрен свой уникальный способ обработки? Подобный вопрос поднимался, например, на конференции CppCon 2018 в докладе Erik Valkering. Smart References. There and Back Again.
Такой код просто не соберется
using FileName = string;
using Url = string;
auto read ( FileName file_name ) { /*read from disk*/ }
auto read ( Url url ) { /*read from internet*/ }
auto test ()
{
auto filename = FileName{ "foobar.txt" };
auto url = Url{ "http://foobar.com/" };
cout << "From disk [" << filename << "]: " read(filename) << endl;
cout << "From web [" << url << "]: " read(url) << endl;
}
А подобный соберется запросто (пример с конференции модифицирован для демонстрации средств библиотеки feature)
using Filename = Wrapper< string, Inplace::Typedef< class Filename_tag > >;
using Url = Wrapper< string, Inplace::Typedef< class Url_tag > >;
auto read ( Filename filename ) { /*read from disk*/ }
auto read ( Url url ) { /*read from internet*/ }
auto test ()
{
auto filename = Filename{ "foobar.txt" };
auto url = Url{ "http://foobar.com/" };
cout << "From disk [" << *&filename << "]: " << read(filename) << endl;
cout << "From web [" << *&url << "]: " << read(url) << endl;
}
Thread safe
А что, если хочется сделать любой объект потокобезопасным? Тогда можно использовать подобное решение
using Map = map< string, pair< string, int > >;
using AtomicMutexMap = Wrapper< Map, ThreadSafe::Atomic >;
void func ()
{
test_map[ "apple" ]->first = "fruit";
test_map[ "potato" ]->first = "vegetable";
for ( size_t i = 0; i < 100000; ++i )
{
test_map->at( "apple" ).second++;
test_map->find( "potato" )->second.second++;
}
auto read_ptr = &as_const( test_map );
cout
<< "potato is " << read_ptr->at( "potato" ).first
<< " " << read_ptr->at( "potato" ).second
<< ", apple is " << read_ptr->at( "apple" ).first
<< " " << read_ptr->at( "apple" ).second
<< endl;
}
void example ()
{
AtomicMutexMap test_map;
vector< thread > threads( thread::hardware_concurrency() );
for ( auto & t : threads ) t = thread( func, test_map );
for ( auto & t : threads ) t.join();
}
Пример взят и модифицирован из статьи Делаем любой объект потокобезопасным
Implicit Sharing
Eсли появилось желание применить технику Copy-on-write (COW), известную также как неявное обобщение Implicit Sharing, широко применяемое в известной библиотеке Qt, то средства библиотеки feature легко позволяют это сделать простой декларацией собственного типа String.
using String = Wrapper< std::string, Implicit::Shared >;
void example ()
{
String first{ "Hello" };
String second = first; // implicit sharing
first += " World!"; // copying on write
}
Optional
В стандарте C++17 в обиход введен весьма полезный класс-обертка std::optional для удобной работы с опциональными значениями. Подобная функциональность может быть достигнута с помощью средств библиотеки feature так же легко:
using OptionalString = Wrapper< std::string, Inplace::Optional >;
OptionalString create( bool b )
{
if (b)
return "Godzilla";
return {};
}
int example ()
{
cout << "create(true) returned "
<< create( true ).value() << endl;
cout << "create(false) returned "
<< create( false ).valueOr( "empty" ) << endl;
}
Дополнительный интерфейс value и valueOr реализуется с помощью приема "подмешивания" функциональности MixIn, реализацию которого рассмотрим чуть ниже.
По своей сути, прием "подмешивания" функциональности позволяет реализовать любой интерфейс для объекта типа Wrapper, в том числе адаптировать или полностью рефлексировать интерфейс для конкретного типа и/или инструмента.
Еще что-то?
Конечно! Здесь рассмотрены далеко не все возможные особенности, которые могут быть дополнительно применены к типам. Средства библиотеки feature позволяют пользователю довольно гибко добавлять собственные дополнительные особенности, например, использовать отложенные или фоновые вычисления, управлять распределением объектов в памяти, реализовывать кеширование данных и формировать любую другую функциональность.
Для этого необходимо реализовать свой собственный так называемый инструмент применения особенностей, который подробнее рассмотрим в разделе с описанием архитектуры feature.
Суперпозиция особенностей
А что если требуется применить сразу несколько дополнительных особенностей? В этом случае средства библиотеки feature позволяют использовать их суперпозицию.
Например, если требуется определить тип для потокобезопасного (thread safe) неявно обобщённого (implicit shared) объекта типа std::string, то это может быть сделано так
using String = Wrapper< Wrapper< std::string, Implicit::Shared > ThreadSafe::Mutex >;
либо так (результат эквивалентный)
using String = Wrapper< std::string, Implicit::Shared, ThreadSafe::Mutex >;
Можно перечислить любое количество дополнительных особенностей, применение которых происходит в порядке "от последнего к первому".
То есть, если определить такой тип
using String = Wrapper< std::string, ThreadSafe::Mutex, Implicit::Shared >;
то его следует читать, как неявное обобщение потокобезопасного объекта типа std::string, что не является эквивалентом определенного выше, и, в конечном счете, не гарантирует его потокобезопасность из-за того, что последним применено непотокобезопасное свойство неявного обобщения.
Архитектура feature
Тип умной ссылки Wrapper
Основным типом данных, который предоставляет библиотека feature, является тип Wrapper из пространства имен ScL::Feature.
namespace ScL { namespace Feature {
template < typename _Value, typename ... _Tools >
using Wrapper; // computable type
}}
Тип Wrapper
представляет собой умную обертку с рефлексией всех конструкторов и всех видов операторов, кроме оператора извлечения адреса operator &
.
Функциональность типа Wrapper
определяется реализацией инструмента _Tools
, которые указываются в качестве параметров шаблона следующими после типа _Value
. Собственно, экземпляр типа Wrapper
агрегирует экземпляр типа _Value
, владеет им, управляет временем его жизни, обеспечивает применение дополнительных свойств и предоставляет доступ к экземпляру типа _Value
посредством механизмов, реализованных в инструментах _Tools
.
Инструмент
Тип инструмента введен для удобства и компактности определения Wrapper
и по сути играет роль пространства имен, в котором должен быть реализован шаблонный тип Holder
template < typename _Value >
struct Holder;
Интерфейс типа Holder
должен иметь все возможные виды реализации конструкторов, которые могут потребоваться при его использовании. Как правило, это означает обеспечение наличия конструктора для любого типа данных _Value
.
template < typename _Value >
struct Holder
{
using ThisType = Holder< _Value >;
using Value = _Value;
template < typename ... _Arguments >
Holder ( _Arguments && ... arguments );
// ...
};
Для обеспечения доступа к значению типа _Value
реализация Holder
должна иметь реализацию методов value
на все возможные случаи использования
template < typename _Value >
struct Holder
{
using ThisType = Holder< _Value >;
using Value = _Value;
static Value && value ( ThisType && holder );
static const Value && value ( const ThisType && holder );
static volatile Value && value ( volatile ThisType && holder );
static const volatile Value && value ( const volatile ThisType && holder );
static Value & value ( ThisType & holder );
static const Value & value ( const ThisType & holder );
static volatile Value & value ( volatile ThisType & holder );
static const volatile Value & value ( const volatile ThisType & holder );
};
Эти методы обеспечивают доступ к значению с сохранением квалификаторов доступа const
, volatile
и типа ссылки rvalue
/lvalue
. Допускается реализация в виде шаблона, но с сохранением вышеперечисленных свойств.
Теперь самое интересное! Обеспечение реализации той или иной дополнительной особенности достигается с помощью опциональной реализации соответствующих методов guard
/unguard
.
template < typename _Value >
struct Holder
{
using ThisType = Holder< _Value >;
using Value = _Value;
static void guard ( ThisType && );
static void guard ( const ThisType && );
static void guard ( volatile ThisType && );
static void guard ( const volatile ThisType && );
static void guard ( ThisType & );
static void guard ( const ThisType & );
static void guard ( volatile ThisType & );
static void guard ( const volatile ThisType & );
static void unguard ( ThisType && );
static void unguard ( const ThisType && );
static void unguard ( volatile ThisType && );
static void unguard ( const volatile ThisType && );
static void unguard ( ThisType & );
static void unguard ( const ThisType & );
static void unguard ( volatile ThisType & );
static void unguard ( const volatile ThisType & );
};
Методы реализуются только на случаи их особого использования. При отсутствии их реализации не вызывается ничего.
Для доступа к значению для экземпляра объекта умной ссылки типа Wrapper реализуется следующий порядок вызовов методов:
определяется контекст использования умной ссылки - квалификаторы доступа и тип ссылки;
вызывается соответствующий метод
guard
(при наличии реализации), который обеспечивает реализацию какого-либо свойства;вызывается соответствующий метод
value
;осуществляется работа с экземпляром значения типа
_Value
в месте вызова;вызывается соответствующий метод
unguard
(при наличии реализации), который обеспечивает утилизацию свойства, реализованного вguard
.
Синтаксис
Для реализации работы с экземплярами умной ссылки типа Wrapper
можно добиться использования синтаксиса, полностью совместимого с внутренним типом _Value
. Достигается это с помощью вспомогательного типа умного указателя ValuePointer
template < typename _WrapperRefer >
class ValuePointer;
Реализация оператора извлечения адреса operator &
для типа Wrapper
возвращает значение типа ValuePointer
, в конструкторе которого вызывается метод guard
, а в деструкторе unguard
.Таким образом, во время существования экземпляра значения типа ValuePointer
гарантируется применение свойств, реализованных в соответствующих инструментах.
В свою очередь, применение оператора разыменования operator *
к указателю типа ValuePointer
предоставляет доступ к внутреннему значению, для которого сохраняются все свойства квалификаторов const
, volatile
и типа ссылки rvalue
/lvalue
.
Следующий пример демонстрирует возможность применения умной обертки Wrapper с сохранением синтаксиса, совместимого с внутренним типом данных.
struct MyType
{
int m_int{};
double m_double{};
string m_string{};
};
template < typename _Type >
void print ( const _Type & value )
{
using namespace std;
cout << "int: " << (*&value).m_int << endl
<< "double: " << (*&value).m_double << endl
<< "string: " << (*&value).m_string << endl;
}
void foo ()
{
using namespace ScL::Feature;
print( MyType{} );
print( Wrapper< MyType >{} );
print( Wrapper< MyType, Implicit::Raw >{} );
}
Доступ к членам экземпляра объекта
Доступ к членам экземпляра объекта осуществляется для ссылки с помощью operator .
, а для указателя с помощью operator ->
. При этом оператор доступа для указателя может быть перегружен и имеет уникальное свойство - его вызов будет многократно разворачиваться до тех пор, пока это возможно, что позволяет использовать широко известную Execute Around Pointer Idiom.
Подобная возможность отсутствует для operator .
, хотя на этот случай имеются несколько предложений в стандарт С++, например, P0416(N4477) или P0352. Пока ни одно из предложений не реализовано, доступ к членам экземпляра объекта через обертку типа Wrapper
реализован с помощью оператора operator ->
, как и для обертки из стандартной библиотеки std::optional.
struct MyType
{
int m_int{};
double m_double{};
string m_string{};
};
void bar ()
{
Wrapper< MyType > value{};
value->m_int = 1;
value->m_double = 2.0;
value->m_string = "three";
}
Такой синтаксис не совместим с базовым и не отражает, что значение value
является умной ссылкой, а не указателем.
Рефлексия операторов
Чтобы сохранять привычный синтаксис при использовании экземпляров значений типа Wrapper
в алгебраических выражениях, средства библиотеки feature реализуют полную рефлексию всех операторов, доступных для внутреннего типа данных. Операторы возвращают умные обертки над возвращаемым результатом оператора базового типа, которые гарантируют применение всех свойств для внутреннего значения на всем протяжении своего существования.
void foo ()
{
using Map = Wrapper< map< int, string > >;
Map m;
m[1] = "one";
m[2] = "two";
}
void foo ()
{
using Int = Wrapper< int >;
Int v{ 16 };
v += 16; // 32
v /= 2; // 16
v <<= 1; // 32
v = ( v * v + 1 ) + v; // 1057
}
Методы std::begin, std::end
Для возможности использования умных оберток для циклов for
, основанных на диапазоне, а также в стандартных алгоритмах, для них реализованы методы std::begin
, std::end
и другие. Эти методы возвращают умные обертки над соответствующими итераторами, которые гарантируют применение всех свойств для контейнера во время существования этих итераторов.
void foo ()
{
using Vector = Wrapper< ::std::vector< int > >;
Vector values{ { 0, 1, 2, 3, 4 } };
for ( const auto & value : values )
cout << *&value << endl;
}
Адаптация к произвольному интерфейсу
В реализацию типа Wrapper библиотеки feature встроена возможность добавления дополнительного интерфейса с помощью приема "подмешивания" функциональности MixIn.
Используя концепцию примесей MixIn имеется возможность подмешать дополнительный интерфейс к реализации "умной" обертки Wrapper
. При этом интерфес может быть подмешан к определенному типу и/или инструменту путем специализации следующего класса
template< typename _Type >
class MixIn {}
Например, для обертки, реализующей опциональность, реализована такая специализация
template< typename _Type >
class MixIn< Detail::Wrapper< _Tool, Inplace::Optional > { /*...*/ }
что позволило добавить методы к интерфейсу value
, valueOr
, emplace
, reset
, swap
, hasValue
и оператор приведения к bool
.
Заключение
Реализция "умных" оберток из состава библиотеки feature позволяют достаточно легко добавлять различные особенности применения к любым пользовательским типам.
Рефлексия операторов и некоторых других методов позволяет использовать функциональность оберток с небольшими изменения кодовой базы.
Реализация библиотеки в виде только заголовочных файлов позволяет легко интегрировать решение в любой проект.
Комментарии (17)
horror_x
10.02.2022 00:50+3
В чём смыслfor ( const auto & value : values ) cout << *&value << endl;
*&
?
P.S. Кодстайл ужасный, читать очень тяжело. Чтобы отличить операторы от ссылок (&
,&&
) и шаблонов (<
,>
), приходится вникать в семантику.sashagil
10.02.2022 04:08+3(я не автор поста) По-моему, смысл & - создать временный объект ValuePointer, с вызовом guard в конструкторе и unguard в деструкторе. Ну а * - собственно взять завёрнутое в этот временный ValuePointer значение, но! - с дополнительными приспоблениями, реализованными в guard / unguard (ну, потоковая безопасность, что-то такое). Польза!
Конечно, невнимательный прикладной программист может случайно забыть добавить *& -- и не получить пользы. Код будет как бы работать, но не совсем: что-то крэшнется, что-то полетит не туда. Ну а кто сказал, что профессия программиста - простая и лёгкая? Программист должен быть внимательным!
horror_x
10.02.2022 06:07Т.е. итераторы враппера разыменовываются не в тип хранимых значений, а во враппер над значением? Враппер же непрозрачным становится, вся соль теряется, плюс не очевидно совсем.
Да и настолько неочевидное использование операторов это ужас какой-то. Это как на условный++
повесить lock, а на--
unlock.
А намёки на потокобезопасность встречаются только в примерах сThreadSafe::*
, так что непонятно, зачем в примере с простейшим случаем вообще какие-то guard'ы.
stepsoft Автор
10.02.2022 08:39+1Кодстайл ужасный, читать очень тяжело.
Совершенно согласен с таким утверждением. Однако, из многочисленных вариантов возможной реализации данный вариант оказался наиболее компактным и совместимым с использованием обычных типов.
В некоторых случаях имеется возможность реализации оператора неявного преобразования обертки к базовому типу, тогда код будет иметь привычный вид.for ( const auto & value : values ) cout << value << endl;
Как раз над этим сейчас и работаю).
sashagil
10.02.2022 16:44+2Если вы продолжаете работать над проектом, обратите, пожалуйста, внимание на правильность английского в идентификаторах, названиях файлов - IsXyzExists плохо звучит. Уберите.Is или замените на Does, убрав s на конце (DoesXyzExist).
stepsoft Автор
10.02.2022 22:42+1Не откладывая в долгий ящик, всё в проекте поправил. Спасибо за замечание, действительно глаз режет.
cdriper
10.02.2022 10:56+3если хочется, чтобы написанных код вызывал хоть какой-то интерес у сообщества, стоит
а) снабжать его максимально удобной и исчерпывающийся документацией на английском языке
б) полностью покрывать юнит тестами (которые, помимо всего прочего, выступают еще и хоть какими-то примерами использования библиотеки)
Kelbon
11.02.2022 09:09Если "потокобезопасность" это добавление на каждую операцию лока, то это не потокобезопасность
if(map.empty())
map.DO ANYTHING()
Это уже не потокобезопасный код, потенциальная гонка и всё прочее. Проще говоря таким способом невозможно сделать что то потокобезопасным, т.к. для этого нужно потокобезопасный интерфейс сам по себе.Да и вообще невозможно на С++ сделать "strong typedef" универсальный в шаблоне. Этим будет невозможно пользоваться.
Ну и сложность гит проекта поражает(не в хорошем смысле) + нужно указывать, что вы используете нестандартные возможности(баги) языка типа loopholes
https://gitlab.com/ssoft-scl/module/model-kit/-/blob/master/src/Meta/Counter.hstepsoft Автор
11.02.2022 11:58Если "потокобезопасность" это добавление на каждую операцию лока, то это не потокобезопасность
Средства библиотеки позволяют сделать операцию lock, как на каждую операцию отдельно, так и на любой блок кода, в зависимости от желаний разработчика. На постоянной основе сейчас можно сделать блокировку так
using Map = Wrapper< map, ThreadSafe::RecursiveMutex >; Map map; void foo () { // в месте, где нужен постоянный lock auto map_ptr = ↦ // map_ptr - умный указатель, который выполнил lock if ( map_ptr->empty() ) map_ptr->doAnything(); }
Playa
За такую кодяру нужно по рукам бить. Сиди потом, разбирайся, что такое FileName, а что такое Url.
Malizia
Если бы FileName и Url были бы отдельными типами, то было бы удобно. А так, в статье, это просто синонимы строк, что, действительно, сбивает с толку.
stepsoft Автор
Подход strong typedef к простой декларации новых типов на основе существующих известен достаточно давно и имеет свою реализацию, например, в Boost.
Здесь показана возможность реализации такого подхода с помощью "умной" обертки на основе примера из доклада на коференции CppCon 2018.
И FileName и Url являются отдельными типами, а не синонимами. В этом весь смысл.
cdriper
это очень известный паттер, который поддерживает множество авторитетных людей в языке, включая Страуструпа
например, есть масса единиц измерения: метры, килограммы, секунды и так далее. и он все могут представлены как float (или там double)
и такого рода подходы позволяют полностью отследить корректность этих единиц во всей программе, что спасает от большого количества разных ошибок
4eyes
Ага, а потом рыцарь ордена битья рук линейкой пишет что-нибудь вроде:
И сиди потом разбирайся, в каких единицах параметр. И через год исправляй ошибки за кем-то, кто неглядя решил, что там секунды.
Я бы лучше написал миллисекунды, и если уж кому-нибудь станет интересно, что у этих миллисекунд под капотом, то пускай он как-нибудь наведёт курсор на имя типа, чтоб IDE ему показала определение.
light_and_ray
Вы жестокий человек, если считаете нормальным по какому-то такому мелочному вопросу бить людей по рукам ????
4eyes
Комментарий удален по просьбе правообладателя (ответил невпопад)