Skip to content

A personal take on strong alias (aka phantom type/strong typedef)

License

Notifications You must be signed in to change notification settings

FabienPean/strong_alias

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 

Repository files navigation

strong_alias

In C++, keywords typedef and using are core language tools to alias a type. It can help shorten the name or constrain templates. However, the introduced names are weak alias, they do not create a new type of that name. In short, assuming that using A = int; is introduced, functions void foo(A){} and void foo(int){} are identical to the compiler, and as such, not a valid overload set.

Strong alias (aka strong typedef/opaque typedef/phantom type) introduces a new name to the compiler, while offering the full or partial set of features of the type that it should alias. They are main idea for libraries such as units libraries or the A basic implementation is straightforward, however it is generally too lax and/or too restrictive on some specific aspects.

namespace simple_v1
{
    template<typename T>
    struct alias
    {
        using T::T;
    };

    struct my_type : alias<Eigen::Vector3d>{ using alias::alias; };
    struct my_other_type : alias<Eigen::Vector3d>{ using alias::alias; };

	void foo(my_type) {
	    foo(my_other_type{});// compiles, not cool
    }
}
namespace simple_v2 
{
    template<typename T>
    struct alias : T
    {
        template<typename... Args> 
        explicit alias(Args&&... args) : T(std::forward<Args>(args)...) {} 
    };

	struct my_type : alias<Eigen::Vector3d>{ using alias::alias; };

    void foo() {
        my_type result = my_type{} + my_type{};// does not compile
    }
}

Moreover, these snippets can only cover class kind of types since it is not possible to use inheritance with a fundamental type. A version for fundamental types can be made relatively easily too, but with the same caveats as for class-type version applies.

namespace alias_for_fundamental_types  
	template <typename T>
    struct alias
    {
        alias() = default;
        template<typename Arg> 
        explicit alias(Arg&& arg) : value(std::forward<Arg>(arg)) {}   

        // Implicitly convert to the underlying value
        constexpr operator       T& ()      &  noexcept { return value; }
        constexpr operator const T& () const&  noexcept { return value; }
        constexpr operator       T&&()      && noexcept { return std::move(value); }
    private:
        T value;
    };

    struct my_type : alias<int>{ };
    struct my_other_type : alias<int>{ };
    auto foo(my_type) {
        my_type result;
        my_other_type other;
        result = result+1;// No
        result += other; // compiles, not cool
        result++;//Yes
    }
}

Many libraries (NamedType, type_safe, strong_type,strong_typedef,strong_type) are available online for doing it. They generally tend to encourage (force) the user to tune in details the properties of the new type with respect to the aliased type. As a result, they are quite cumbersome to use for simple aliasing.

The strong alias utility contained in this repository does not allow tuning, with simples predicates on the alias. It is a simple utility with fixed properties, which are:

  • ❌ forbids
    • Direct initialization from another alias (A a; B b = a;)
    • Any kind of assignment from another alias (A a; B b += a;)
    • Passing a different alias as function argument without explicit cast (void foo(B); f(A{}); )
    • Comparison to a different alias without explicit casting (A a; B b; a == b;)
  • ✔️ allows
    • Explicit conversion from another alias (A a; B b{a};)
    • Implicit conversion from any other source, particularly convenient in the context of smart expression template engine (A a; B b; b = a*2;)
    • Everything else, the availability of any operation (and compilation error) are generated by the underlying type

Example

This is but an excerpt. The complete list is visible at the bottom of strong_alias.h

#include <strong_alias.h>
#include <Eigen/Dense>
#include <cstdint>

ALIAS(A, std::int32_t);
ALIAS(B, std::int32_t);

ALIAS(X, Eigen::Vector3d);
ALIAS(Y, Eigen::Vector3d);

int main()
{
    /// Fundamental type alias
    /////////////////////////////////////////////
    { A a; A b{ std::int8_t{42} }; }        // ✔️
    { A a; A b{ a }; }                      // ✔️
    { A a; A  b = a; }                      // ✔️
    { A a, b; A a3 = a + b; }               // ✔️
    { A a; A b; a += b; }                   // ✔️
    { A a; A b; a == b; }                   // ✔️
    { A a; a++; ++a; --a; a--; }            // ✔️
    { A a; a == 1; }                        // ✔️
    { A a; B b; a = b + 5; }                // ✔️
    { A a; B b(a); }                        // ✔️
    { A a; B b(a + 1); }                    // ✔️
    { A a; B b; a += b; }                   //
    { A a; B b; a == b; }                   //
    { A a; B b = a; }                       //
    { A a; B b; b = a; }                    //
    { A a; [](B)  {} (a); }                 //
    { A a; [](B&) {} (a); }                 //
    { A a; [](B&&){} (std::move(a)); }      //


    ///// Class type alias
    /////////////////////////////////////////////
    { X a; X b{ 42.,3.14,2.4 }; }           // ✔️   
    { X a; X b{ a }; }                      // ✔️   
    { X a; X b; b = a; }                    // ✔️   
    { X a; X b; X a3 = a + b; }             // ✔️   
    { X a; X b; a += b; }                   // ✔️
    { X a; X b; a == b; }                   // ✔️
    { X a; Y b; a = b.array() + 5; }        // ✔️   
    { X a; Y b(a); }                        // ✔️    
    { X a; Y b(a.array() + 1); }            // ✔️    
    { X a; Y b; a += b; }                   //
    { X a; Y b; a == b; }                   //
    { X a; Y b = a; }                       //
    { X a; Y b; b = a; }                    //
    { X a; [](Y)  {} (a); }                 //
    { X a; [](Y&) {} (a); }                 //
    { X a; [](Y&&){} (std::move(a)); }      //

    return 0;
}

Learnings

Various ways of allowing/disabling specific overload

template<typename T, typename... Others>
struct alias 
{
    T value;
    
    template<typename...>
    constexpr bool condition = /*...*/;

    // v1: enabling only desired types
    template<typename Arg, typename = std::enable_if_t<condition<Arg>>>
    constexpr alias& operator+=(const Arg& arg) { value += arg; return *this; }

    // v2: deleting undesired overload
    template<typename... Arg>
    constexpr alias& operator+=(const Arg&... arg) { value += (arg,...); return *this; }
    template<typename Arg, typename = std::enable_if_t<condition<Arg>>>
    constexpr alias& operator+=(const Arg& arg) = delete;

    // v3: static_assert
    template<typename Arg>
    constexpr alias& operator+=(const Arg& arg) {
        constexpr bool condition = /*...*/;
        static_assert(condition, "Condition not respected");
        value += arg; return *this;
    }
};

Depending on the operator, v1 may not be feasible. For example, with comparison operators living in the global namespace, defining or deleting the overload is mandatory because the implicit conversion scheme would apply and therefore still works.

SFINAE using validity of an expression

Imagining we want to enable operation iff the underlying object has the operation already defined. The combination of std::void_t, decltype and std::declval does the job. In particular, std::void_t was made specifically for triggering SFINAE based on type well-formedness. decltype is a core feature returning the type of an (unevaluated) expression or entity, and std::declval allows to use objects without constructing them (constructing an object requires an evaluated context)

template<typename = std::void_t<decltype(std::declval<T&>()--)>>
T  operator--(int) { return value--; };

Templates within a preprocessor macro

The preprocessor is a independent tool with its own rules and which is unaware of C++ syntax. It is a kind of glorified string search & replace that occurs at an early step of the compilation process, when the source code is still understood as a blob of characters. It is possible to define function-like macros, for which the argument separator is the comma. Providing an argument containing commas is feasible as long as the argument is wrapped within some brackets. However, this cannot be always done for templates because it is not a valid syntax for C++ in a later compilation stage. Therefore, using templates within a call to a macro can lead to preprocessor errors or compiler errors. For example:

//PROBLEMS
#define DECLARE(NAME, TYPE) \
struct NAME : TYPE {}
DECLARE(vec, std::vector<double>);                //✔️
DECLARE(dict1, std::unordered_map<int, double>);  //❌ error: macro "DECLARE" passed 3 arguments, but takes just 2
//     ┃➥#1┃         ➥#2           ┃  ➥#3  ┃
DECLARE(dict2, (std::unordered_map<int, double>));//❌ error: invalid declarator before ')' token

//WORKAROUND#1: Defining a transparent alias without the undesirable characters
using umap = std::unordered_map<int, double>;
DECLARE(dict, umap);                             //✔️

//WORKAROUND#2: Always wrapping the TYPE within brackets, and resort to a nested preprocessor call
#define DUMMY(...) __VA_ARGS__
#define DECLARE(NAME, TYPE) \
struct NAME : DUMMY TYPE {} // DUMMY TYPE will be replaced by TYPE without leading and trailing bracket
DECLARE(vec, (std::vector<double>));              //✔️
DECLARE(dict, (std::unordered_map<int, double>));//✔️

For the specific case where the template parameter to pass around is the last argument of the function-like macro, a simpler solution can be obtained with the help of variadic macro.

#define ALIAS(NAME, ...) \
NAME : __VA_ARGS__ {}

Non-static member function qualifiers

It is common to see a const qualifier slapped on a non-static member function after the argument list, which signifies that a method can operate only on const object. On the other hand, the ref qualifier versions are more rare.

template<typename T>
class wrap {
public:
    //                v
    operator T() const& { ... } //Enable implicit conversion to T when instance of wrap is a const wrap&
    operator T() &&     { ... } //Enable implicit conversion to T when instance of wrap is a wrap&&
    //           ^
private:
   T data;
};

License

MIT

About

A personal take on strong alias (aka phantom type/strong typedef)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages