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

13 Commits
 
 
 
 
 
 

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. A basic implementation is straightforward, however it is generally too lax or too restrictive.

Many libraries (NamedType, type_safe, strong_type,strong_typedef,strong_type) are available for crafting some strong types. 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 minimal predicates on the alias. It is a relatively 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; b += a;)
    • Passing a different alias as function argument without explicit cast (void f(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. A more complete list is visible at the end 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;
}

One-liner implementations and deficiencies

// Basic v1: too lax
template<typename T>
struct alias : T
{
    using T::T;
};
// Problem
#include <Eigen/Dense>
struct my_type : alias<Eigen::Vector3d>{ using alias::alias; };
struct my_other_type : alias<Eigen::Vector3d>{ using alias::alias; };
void foo(my_type) {}

int main() {
    foo(my_other_type{});// compiles, not cool ☹️
}
// Basic v2: too restrictive
template<typename T>
struct alias : T
{
    template<typename... Args> 
    explicit alias(Args&&... args) : T(std::forward<Args>(args)...) {} 
};
// Problem
#include <Eigen/Dense>
struct my_type : alias<Eigen::Vector3d>{ using alias::alias; };

int main() {
    my_type result = my_type{} + my_type{};// does not compile, not cool ☹️
}

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

// Basic for scalar types
template <typename T>
struct alias
{
    // 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); }

    T value = {};
};
// Problem
struct my_type : alias<int>{};
struct my_other_type : alias<int>{ };

int main() {
    my_type result{5};
    my_other_type other;
    result = result+1;// does not compile, missing operator=
    result += other;  // compiles, not cool
    other.value = 5;  // not private
}

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