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;
)
- Direct initialization from another alias (
- ✔️ 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
- Explicit conversion from another alias (
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;
}
// 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
}
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.
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--; };
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__ {}
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;
};
MIT