Представляю сообществу библиотеку 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 позволяют достаточно легко добавлять различные особенности применения к любым пользовательским типам.

Рефлексия операторов и некоторых других методов позволяет использовать функциональность оберток с небольшими изменения кодовой базы.

Реализация библиотеки в виде только заголовочных файлов позволяет легко интегрировать решение в любой проект.

Проект иструментов ScL доступен по ссылке

Комментарии (17)


  1. Playa
    09.02.2022 23:50
    +2

    auto read ( FileName file_name );
    auto read ( Url url );

    За такую кодяру нужно по рукам бить. Сиди потом, разбирайся, что такое FileName, а что такое Url.


    1. Malizia
      10.02.2022 00:14
      +3

      Если бы FileName и Url были бы отдельными типами, то было бы удобно. А так, в статье, это просто синонимы строк, что, действительно, сбивает с толку.


      1. stepsoft Автор
        10.02.2022 08:28
        +6

        Подход strong typedef к простой декларации новых типов на основе существующих известен достаточно давно и имеет свою реализацию, например, в Boost.
        Здесь показана возможность реализации такого подхода с помощью "умной" обертки на основе примера из доклада на коференции CppCon 2018.

        И FileName и Url являются отдельными типами, а не синонимами. В этом весь смысл.


    1. cdriper
      10.02.2022 10:54
      +5

      это очень известный паттер, который поддерживает множество авторитетных людей в языке, включая Страуструпа

      например, есть масса единиц измерения: метры, килограммы, секунды и так далее. и он все могут представлены как float (или там double)

      и такого рода подходы позволяют полностью отследить корректность этих единиц во всей программе, что спасает от большого количества разных ошибок


    1. 4eyes
      10.02.2022 23:24
      +2

      Ага, а потом рыцарь ордена битья рук линейкой пишет что-нибудь вроде:

      void set_duration(int first_delay, int loop_duration);

      И сиди потом разбирайся, в каких единицах параметр. И через год исправляй ошибки за кем-то, кто неглядя решил, что там секунды.

      Я бы лучше написал миллисекунды, и если уж кому-нибудь станет интересно, что у этих миллисекунд под капотом, то пускай он как-нибудь наведёт курсор на имя типа, чтоб IDE ему показала определение.


    1. light_and_ray
      11.02.2022 02:25
      +1

      Вы жестокий человек, если считаете нормальным по какому-то такому мелочному вопросу бить людей по рукам ????


      1. 4eyes
        11.02.2022 02:44

        Комментарий удален по просьбе правообладателя (ответил невпопад)


  1. thatsme
    10.02.2022 00:42

    del


  1. horror_x
    10.02.2022 00:50
    +3

        for ( const auto & value : values )
            cout << *&value << endl;
    В чём смысл *&?

    P.S. Кодстайл ужасный, читать очень тяжело. Чтобы отличить операторы от ссылок (&, &&) и шаблонов (<, >), приходится вникать в семантику.


    1. sashagil
      10.02.2022 04:08
      +3

      (я не автор поста) По-моему, смысл & - создать временный объект ValuePointer, с вызовом guard в конструкторе и unguard в деструкторе. Ну а * - собственно взять завёрнутое в этот временный ValuePointer значение, но! - с дополнительными приспоблениями, реализованными в guard / unguard (ну, потоковая безопасность, что-то такое). Польза!

      Конечно, невнимательный прикладной программист может случайно забыть добавить *& -- и не получить пользы. Код будет как бы работать, но не совсем: что-то крэшнется, что-то полетит не туда. Ну а кто сказал, что профессия программиста - простая и лёгкая? Программист должен быть внимательным!


      1. horror_x
        10.02.2022 06:07

        Т.е. итераторы враппера разыменовываются не в тип хранимых значений, а во враппер над значением? Враппер же непрозрачным становится, вся соль теряется, плюс не очевидно совсем.

        Да и настолько неочевидное использование операторов это ужас какой-то. Это как на условный ++ повесить lock, а на -- unlock.

        А намёки на потокобезопасность встречаются только в примерах с ThreadSafe::*, так что непонятно, зачем в примере с простейшим случаем вообще какие-то guard'ы.


    1. stepsoft Автор
      10.02.2022 08:39
      +1

      Кодстайл ужасный, читать очень тяжело.

      Совершенно согласен с таким утверждением. Однако, из многочисленных вариантов возможной реализации данный вариант оказался наиболее компактным и совместимым с использованием обычных типов.
      В некоторых случаях имеется возможность реализации оператора неявного преобразования обертки к базовому типу, тогда код будет иметь привычный вид.

      for ( const auto & value : values )
              cout << value << endl;

      Как раз над этим сейчас и работаю).


      1. sashagil
        10.02.2022 16:44
        +2

        Если вы продолжаете работать над проектом, обратите, пожалуйста, внимание на правильность английского в идентификаторах, названиях файлов - IsXyzExists плохо звучит. Уберите.Is или замените на Does, убрав s на конце (DoesXyzExist).


        1. stepsoft Автор
          10.02.2022 22:42
          +1

          Не откладывая в долгий ящик, всё в проекте поправил. Спасибо за замечание, действительно глаз режет.


  1. cdriper
    10.02.2022 10:56
    +3

    если хочется, чтобы написанных код вызывал хоть какой-то интерес у сообщества, стоит

    а) снабжать его максимально удобной и исчерпывающийся документацией на английском языке

    б) полностью покрывать юнит тестами (которые, помимо всего прочего, выступают еще и хоть какими-то примерами использования библиотеки)


  1. 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.h


    1. stepsoft Автор
      11.02.2022 11:58

      Если "потокобезопасность" это добавление на каждую операцию лока, то это не потокобезопасность

      Средства библиотеки позволяют сделать операцию lock, как на каждую операцию отдельно, так и на любой блок кода, в зависимости от желаний разработчика. На постоянной основе сейчас можно сделать блокировку так

      using Map = Wrapper< map, ThreadSafe::RecursiveMutex >;
      Map map;
      
      void foo ()
      {
          // в месте, где нужен постоянный lock
          auto map_ptr = &map;
              // map_ptr - умный указатель, который выполнил lock
          if ( map_ptr->empty() )
              map_ptr->doAnything();
      }