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