Добавляем дополнительные особенности реализации на C++ с помощью «умных» оберток
Представляю сообществу библиотеку 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: opational.
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 позволяют достаточно легко добавлять различные особенности применения к любым пользовательским типам.
Рефлексия операторов и некоторых других методов позволяет использовать функциональность оберток с небольшими изменения кодовой базы.
Реализация библиотеки в виде только заголовочных файлов позволяет легко интегрировать решение в любой проект.
Проект иструментов ScL доступен по ссылке