Adding Additional C++ Implementation Features with Smart Wrappers

I present to the community the feature library from the composition of the libraries I am developing under the general name ScL. The ScL library set itself systematizes a fairly modest set of implementations and approaches that, in my opinion, can simplify the process of software development in C ++.

The feature library tools allow you to endow instances of objects of any type with properties that they do not initially have. Such properties include, for example, lazy evaluations (implicit shared and others), thread safety, choice of how to place an object “in place” or on the “heap”, etc.

All of the listed properties are added using the implementation of the “smart” wrappers mechanism, the basic set of which is presented in the feature library, and can be easily extended by any custom solutions.

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!"
}

Do you want to know how? Please under cat.

Motivation

Do you feel annoyed when the found ready-made solution does not satisfy the necessary properties for possible use in your implementation? And you have to create cumbersome, often the same type of additional functionality on top of what is already there. Or, more often, write your own implementation of the next bike that differs in color, frame or wheel shape.

Here are some cases of desirable additional functionality and examples of their implementation.

strong typedef

Programs often use the same type to declare completely incompatible concepts. For example, the std::string type can represent url, e-mail, full name, address, etc. And what if each of these types has its own unique way of processing? A similar question was raised, for example, at a conference Cppcon 2018 in the report Erik Valkering. smart references. There and Back Again.

This code just won’t work.

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;
}

And a similar one will be assembled easily (an example from the conference is modified as an example of using the feature library tools)

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

But what if you want to make any object thread-safe? Then you can use a similar solution

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();
}

The example is taken and modified from the article Making any object thread-safe

Implicit Sharing

If there is a desire to apply the technique Copy-on-write (COW)also known as implicit generalization Implicit Sharingwhich is widely used in the well-known Qt library, the feature library tools make it easy to do this with a simple declaration of your own String type.

using String = Wrapper< std::string, Implicit::Shared >;

void example ()
{
    String first{ "Hello" };
    String second = first; // implicit sharing
	  first += " World!";    // copying on write  
}

Optional

The C++17 standard introduces a very useful wrapper class std:: optional for convenient work with optional values. Similar functionality can be achieved using the feature library tools just as easily.

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;
}

The additional interface value and valueOr are implemented using the technique of “mixing” functionality mixinwhose implementation will be discussed below.

At its core, the technique of “mixing” functionality allows you to implement any interface for an object of the Wrapper type, including adapting or completely reflecting the interface for a particular type and/or tool.

Something else?

Certainly! Not all possible features that can be additionally applied to types are considered here. The feature library tools allow the user to quite flexibly add their own additional features, for example, use deferred or background calculations, manage the distribution of objects in memory, implement data caching, and form any other functionality.

To do this, you need to implement your own, so-called feature application tool, which we will discuss in more detail in the feature architecture section.

Superposition of singularities

But what if you want to apply several additional features at once? In this case, the feature library tools allow you to use their superposition.

For example, if you want to define a type for a thread-safe (thread safe) implicitly shared (implicit shared) object of type std::string, then this can be done like this

using String = Wrapper< Wrapper< std::string, Implicit::Shared > ThreadSafe::Mutex >;

or so (the result is equivalent)

using String = Wrapper< std::string, Implicit::Shared, ThreadSafe::Mutex >;

Any number of optional features can be listed, applied in order from last to first.

That is, if we define such a type

using String = Wrapper< std::string, ThreadSafe::Mutex, Implicit::Shared >;

then it should be read as an implicit genericization of a thread-safe object of type std::string, which is not equivalent to the one defined above, and ultimately does not guarantee its thread-safety due to the non-thread-safe property of implicit genericization being applied last.

Architecture

Wrapper smart link type

The main data type provided by the feature library is the Wrapper type from the ScL::Feature namespace.

namespace ScL { namespace Feature {
    template < typename _Value, typename ... _Tools >
    using Wrapper; // computable type
}}

A type Wrapper is a smart wrapper with reflection of all constructors and all kinds of operators, except for the address extraction operator operator &.

Type functionality Wrapper defined by toolkit implementation _Toolswhich are specified as template parameters next after the type _Value. Actually, an instance of the type Wrapper aggregates an instance of a type _Valueowns it, manages its lifetime, enforces additional properties, and provides access to an instance of the type _Value through the mechanisms implemented in the toolkits _Tools.

Tools

The toolkit type is introduced for convenience and compactness of the definition Wrapper and essentially plays the role of the namespace in which the template type must be implemented Holder

template < typename _Value >
struct Holder;

type interface Holder must have all possible kinds of constructor implementations that may be required when using it. Typically, this means providing a constructor for any data type. _Value.

template < typename _Value >
struct Holder
{
    using ThisType = Holder< _Value >;
    using Value = _Value;

    template < typename ... _Arguments >
    Holder ( _Arguments && ... arguments );
    // ...
};

To provide access to a value of type _Value implementation Holder must have method implementation value for all possible use cases

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 );
};

These methods provide value access while preserving access qualifiers const, volatile and link type rvalue/lvalue. Implementation as a template is allowed, but with the above properties preserved.

Now the fun part! Ensuring the implementation of one or another additional feature is achieved using the optional implementation of the corresponding guard / unguard methods.

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 & );
};

Methods are implemented only for cases of their special use. In the absence of their implementation, nothing is called.

To access a value for an instance of a Wrapper smart link object, the following order of method calls is implemented:

  • the context of using a smart link is determined – access qualifiers and link type;

  • the corresponding method is called guard (if there is an implementation), which provides the implementation of some property;

  • the corresponding method is called value;

  • work with an instance of a value of type _Value at the place of the call;

  • the corresponding method is called unguard (if there is an implementation) that provides utilization of the property implemented in guard.

Syntax

To implement work with smart link instances of type Wrapper it is possible to achieve the use of a syntax that is fully compatible with the internal type _Value. This is achieved using an auxiliary smart pointer type ValuePointer

template < typename _WrapperRefer >
class ValuePointer;

Implementation of the address extraction operator operator & for type Wrapper returns a value of type ValuePointerin the constructor of which the method is called guardand in the destructor unguard.Thus, during the lifetime of an instance of a value of type ValuePointer the application of the properties implemented in the corresponding toolkits is guaranteed.

In turn, the use of the dereference operator operator * to type pointer ValuePointer provides access to the internal value for which all qualifier properties are preserved const, volatile and link type rvalue/lvalue.

The following example demonstrates how a smart Wrapper can be applied while maintaining a syntax that is compatible with the internal data type.

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 >{} );
}

Accessing Object Instance Members

The members of an object instance are accessed by reference using operator .and for the pointer with operator ->. At the same time, the access operator for the pointer can be overloaded and has a unique property – its call will be expanded many times as long as possible, which allows using the well-known Execute Around Pointer Idiom.

There is no such option for operator .although there are several proposals in the C++ standard for this case, for example, P0416(N4477) or P0352. While none of the suggestions are implemented, accessing object instance members through a type wrapper Wrapper implemented using the operator operator ->as for the wrapper from the standard library 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";
}

This syntax is not compatible with the base one and does not reflect that the value value is a smart reference, not a pointer.

Operator Reflection

To preserve the familiar syntax when using instances of values ​​of type Wrapper in algebraic expressions, the feature library means implements a complete reflection of all operators available for the internal data type. Operators return clever wrappers around the return result of the base type operator that ensure that all properties on the inner value are applied for the duration of its existence.

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
}

Methods std::begin, std::end

To be able to use smart wrappers for loops forbased on the range, as well as in standard algorithms, methods are implemented for them std::begin, std::end other. These methods return smart wrappers over the corresponding iterators that ensure that all properties for the container are applied during the lifetime of those iterators.

void foo ()
{
    using Vector = Wrapper< ::std::vector< int > >;

    Vector values{ { 0, 1, 2, 3, 4 } };
  
    for ( const auto & value : values )
        cout << *&value << endl;
}

Adaptation to an arbitrary interface

The Wrapper type implementation of the feature library has a built-in ability to add an additional interface using the “mixing” functionality technique. mixin.

Using the concept of MixIn mixins, it is possible to mix an additional interface into the implementation of a “smart” wrapper Wrapper. In this case, the interface can be mixed into a specific type and / or toolkit by specializing the following class

template< typename _Type >
class MixIn {}

For example, for a wrapper that implements optionality, such a specialization is implemented

template< typename _Type >
class MixIn< Detail::Wrapper< _Tool, Inplace::Optional > { /*...*/ }

which made it possible to add methods to the interface value, valueOr, emplace, reset, swap, hasValue and the cast operator bool.

Conclusion

The implementation of “smart” wrappers from the feature library makes it quite easy to add various application features to any custom types.

Reflection of operators and some other methods allows you to use the functionality of wrappers with minor changes to the code base.

The implementation of the library in the form of only header files makes it easy to integrate the solution into any project.

The ScL Tool Project is available here

Similar Posts

Leave a Reply Cancel reply