Pull to refresh

Добавляем дополнительные особенности реализации на C++ с помощью «умных» оберток

Reading time10 min
Views7.5K

Представляю сообществу библиотеку 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) << "\n";
    cout << "From web  [" << url      << "]: " read(url) << "\n";
}

А подобный соберется запросто (пример с конференции модифицирован для демонстрации средств библиотеки 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) << "\n";
    cout << "From web  [" << url      << "]: " << read(url) << "\n";
}

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
        << "\n";
}

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() << "\n";
    cout << "create(false) returned "
         << create( false ).valueOr( "empty" ) << "\n";
}

Дополнительный интерфейс 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    << "\n"
         << "double: " << (*&value).m_double << "\n"
         << "string: " << (*&value).m_string << "\n";
}

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 << "\n";
}

Адаптация к произвольному интерфейсу

В реализацию типа 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 доступен по ссылке

Tags:
Hubs:
Total votes 9: ↑8 and ↓1+8
Comments19

Articles