Skip to content
C++
Language
since C++98
Basic

Type Aliases

"C++ type aliases: using declarations, typedef, alias templates, dependent type extraction, and pitfalls for working engineers."

Type Aliassince C++98 / C++11

A type alias introduces an alternative name for an existing type without creating a distinct type; typedef (C++98) and using (C++11) are semantically equivalent for simple aliases, but only using can define alias templates.

Overview

Both typedef and using produce transparent synonyms: the alias and its target are the same type to the compiler. Overload resolution, template specialization, and std::is_same all treat them as identical.

cpp
typedef unsigned long long u64;
using u64 = unsigned long long;  // C++11 β€” identical effect

static_assert(std::is_same_v<u64, unsigned long long>);  // C++17 std::is_same_v

The using form should be the default in C++11 and later. It reads left-to-right, handles function pointer types without inside-out parsing, and β€” critically β€” supports alias templates that typedef cannot express.

Syntax

cpp
// C++98: typedef
typedef int (*CompareFn)(const void*, const void*);    // function pointer β€” confusing
typedef std::map<std::string, std::vector<int>> Index; // verbose but legal

// C++11: using β€” same semantics, cleaner syntax
using CompareFn = int(*)(const void*, const void*);
using Index     = std::map<std::string, std::vector<int>>;

// C++11: alias template β€” typedef has no equivalent
template<typename T>
using Vec = std::vector<T>;

template<typename K, typename V>
using Dict = std::unordered_map<K, V>;

The typedef syntax places the new name somewhere in the middle of the declaration β€” particularly disorienting for function pointers and pointer-to-member types. The using syntax always puts the name on the left.

Alias Templates

Alias templates parameterize a type alias over one or more template arguments. The pre-C++11 workaround required a nested type member inside a helper struct; alias templates eliminate that boilerplate entirely.

cpp
// Pre-C++11: "template typedef" via nested struct
template<typename T>
struct VecHelper { typedef std::vector<T> type; };
typename VecHelper<int>::type v;  // verbose call site

// C++11: alias template β€” direct
template<typename T>
using Vec = std::vector<T>;
Vec<int> v;

// Partial binding: fix one argument, leave the other open
template<typename T>
using PoolVec = std::vector<T, PoolAllocator<T>>;

// PMR containers β€” C++17
template<typename T>
using PmrVec = std::pmr::vector<T>;  // C++17

Alias templates are transparent: they expand to the underlying type and do not produce a new independent template. This has important consequences described in the Pitfalls section.

Fixing Allocator Policies

Alias templates let you pin a shared configuration once and propagate it across a related family of types:

cpp
template<typename T>
using PoolString = std::basic_string<char, std::char_traits<char>, PoolAllocator<char>>;

// C++17: polymorphic memory resource variants
template<typename T>
using PmrString = std::basic_string<char, std::char_traits<char>,
                                    std::pmr::polymorphic_allocator<char>>;

Changing the allocator strategy requires editing one alias, not every declaration site.

Dependent Type Extraction

When a type name depends on a template parameter, typename is required. Alias templates can absorb that typename so call-site code stays clean:

cpp
// Without alias: typename at every use
template<typename C>
void process(C& c) {
    typename C::value_type  x  = c.front();
    typename C::iterator    it = c.begin();
}

// C++11: extract once via alias template
template<typename T> using ValueTypeOf = typename T::value_type;
template<typename T> using IteratorOf  = typename T::iterator;

template<typename C>
void process(C& c) {
    ValueTypeOf<C> x  = c.front();
    IteratorOf<C>  it = c.begin();
}

From C++20, concepts constrain template parameters structurally, which often eliminates explicit extraction aliases β€” constrained code can name associated types directly through concept requirements.

Class Member Aliases

STL-compatible containers expose a standardized set of nested aliases so generic algorithms can introspect them without knowing the concrete type:

cpp
class ByteBuffer {
public:
    using value_type      = std::byte;        // C++17
    using size_type       = std::size_t;
    using difference_type = std::ptrdiff_t;
    using pointer         = value_type*;
    using const_pointer   = const value_type*;
    using reference       = value_type&;
    using const_reference = const value_type&;
    using iterator        = pointer;
    using const_iterator  = const_pointer;

    iterator begin() noexcept { return data_; }
    iterator end()   noexcept { return data_ + size_; }

private:
    value_type* data_{};
    size_type   size_{};
};

// Generic algorithm reads value_type without knowing the container
template<typename Container>
auto first_element(const Container& c) -> typename Container::value_type {
    return *c.begin();
}

This pattern is how std::iterator_traits, range-based algorithms, and C++20 std::ranges concepts discover element types at compile time.

Best Practices

Prefer using over typedef unconditionally in C++11 and later. Keep typedef only when maintaining C++03 code or writing headers that must compile as C.

Name aliases with type conventions. Use PascalCase for public-facing aliases (EventHandlerPtr), and lowercase with underscores for STL-style nested aliases (value_type, size_type).

Prefer alias templates over macros for abbreviating long template instantiations. Aliases participate in the type system; macros don't.

cpp
// Avoid: macro loses type identity and doesn't compose with templates
#define STRMAP std::unordered_map<std::string, std::string>

// Prefer: alias participates in overload resolution and deduction
using StrMap = std::unordered_map<std::string, std::string>;

Use alias templates to centralize policy parameters (allocators, deleters, traits) rather than threading them through every declaration.

Common Pitfalls

Alias templates cannot be partially specialized

Alias templates expand transparently. The compiler does not accept partial specializations of them:

cpp
template<typename T>
using Ptr = std::unique_ptr<T>;

// Error: partial specialization of alias template
template<typename T>
using Ptr<T[]> = std::unique_ptr<T[]>;  // ill-formed

The workaround is a traits struct with a nested type member exposed via an alias:

cpp
template<typename T>        struct PtrTraits      { using type = std::unique_ptr<T>; };
template<typename T>        struct PtrTraits<T[]>  { using type = std::unique_ptr<T[]>; };
template<typename T> using  Ptr = typename PtrTraits<T>::type;

CTAD does not fire through alias templates before C++20

Class Template Argument Deduction (C++17) deduces template arguments from constructor calls but does not propagate through alias templates in C++17:

cpp
template<typename T>
using Pair = std::pair<T, int>;

auto p = Pair{3.14, 42};  // ill-formed in C++17 β€” CTAD doesn't apply to alias templates
                           // valid in C++20, which added alias template CTAD

C++20 allows deduction guides to fire through alias templates, making this pattern work.

Template template parameters and alias templates (pre-C++17 mismatch)

Prior to C++17, template template parameters required an exact match on the number of template parameters. Because std::vector<T> has two parameters (T and Allocator), passing an alias template that wraps it to a template template parameter expecting one parameter was ill-formed:

cpp
template<template<typename> class Container>
void fill(Container<int>& c, int val);

template<typename T>
using Vec = std::vector<T>;  // expands to vector<T, allocator<T>> β€” two params

fill<Vec>(v, 0);  // ill-formed in C++14; valid in C++17 (relaxed matching)

C++17 relaxed template template parameter matching to allow this, but if you target C++14 you need a dedicated single-parameter wrapper.

Aliases don't create new types β€” strong typedefs require more work

An alias and its underlying type are the same type in every overload resolution and specialization context:

cpp
using Meters  = double;
using Seconds = double;

void f(Meters m);
void f(Seconds s);  // Error: redefinition β€” both are double

For a genuinely distinct type, use a wrapper struct, enum class with a single enumerator, or a library like boost::units. A plain alias gives you readability, not type safety.

using Declarations in Derived Classes (Name Unhiding)

A using declaration inside a class body imports a name from a base class into the derived class's scope. This is a different construct from a type alias β€” it moves a declaration, not a type β€” but the using keyword is shared.

cpp
// C++98
struct Base {
    void f(int);
    void f(double);
};

struct Derived : Base {
    void f(std::string);  // hides BOTH Base::f(int) and Base::f(double)
};

Derived d;
d.f(1);       // Error: Base::f(int) is hidden
d.f(3.14);    // Error: Base::f(double) is hidden

Adding a using declaration restores the hidden overloads alongside the new one.

cpp
// C++98
struct Derived : Base {
    using Base::f;        // unhides f(int) and f(double)
    void f(std::string);  // adds a third overload
};

Derived d;
d.f(1);           // OK: calls Base::f(int)
d.f(3.14);        // OK: calls Base::f(double)
d.f("hello");     // OK: calls Derived::f(std::string)

using declarations can also change access levels. Placing using Base::protected_member; in a public: section of a derived class promotes it from protected to public access in that derived type.

cpp
// C++98
struct Base {
protected:
    void internal();
};

struct Adapter : Base {
public:
    using Base::internal;  // promoted to public in Adapter
};

Inheriting constructors (C++11). using Base::Base; makes all base class constructors available in the derived class as if they were declared there. This is especially useful for thin wrapper types that add no new data members.

cpp
// C++11
struct NamedInt : std::pair<std::string, int> {
    using std::pair<std::string, int>::pair;  // inherit all pair constructors
};

NamedInt n{"answer", 42};  // calls pair(string, int)

Distinction from type aliases. using Base::f is a using declaration β€” it imports a name into a scope. using Alias = SomeType is a type alias β€” it creates a synonym. The syntax is similar but the semantics are unrelated.


using enum (C++20)

using enum EnumName; brings all enumerators of a scoped (enum class) enumeration into the current scope, eliminating repetitive qualification.

cpp
// C++11: scoped enum requires full qualification everywhere
enum class Color { Red, Green, Blue };

Color c = Color::Red;   // C++11 β€” always need Color::

With using enum, enumerators become directly visible within the enclosing scope:

cpp
// C++20
void describe(Color c) {
    using enum Color;  // Red, Green, Blue now visible unqualified

    if (c == Red)   std::cout << "red\n";
    if (c == Green) std::cout << "green\n";
    if (c == Blue)  std::cout << "blue\n";
}

using enum is most valuable inside switch statements. Every case label would otherwise repeat the enum name, which becomes noise when the switch variable already communicates the type.

cpp
// C++20
enum class Status { Pending, Running, Done, Failed };

std::string to_string(Status s) {
    switch (s) {
        using enum Status;  // Pending, Running, Done, Failed visible below
        case Pending: return "pending";
        case Running: return "running";
        case Done:    return "done";
        case Failed:  return "failed";
    }
    return "unknown";
}

Selective import. To pull in only one enumerator rather than the whole enum, use a targeted using declaration:

cpp
// C++20
using Color::Red;  // only Red is unqualified; Green and Blue still need Color::

This is more surgical than using enum and reduces the risk of name collisions in scopes where two enums share enumerator names.


Phantom Types and Strong Typedef Simulation

Plain aliases don't create distinct types (covered in the Pitfalls section), but phantom types do. The technique attaches a tag type β€” which carries no data and may never be defined β€” to a value wrapper, creating genuine type-system separation.

cpp
// C++11
template<typename Tag, typename T>
struct StrongType {
    explicit StrongType(T v) : value(v) {}
    T value;
};

struct MetersTag {};
struct SecondsTag {};

using Meters  = StrongType<MetersTag,  double>;
using Seconds = StrongType<SecondsTag, double>;

The tags MetersTag and SecondsTag are phantom: they appear only as template arguments and never need a body. Because Meters and Seconds are now different instantiations, the compiler rejects mixing them.

cpp
// C++11
Meters  distance{3.0};
Seconds elapsed{1.5};

auto bad = distance.value + elapsed.value;  // compiles (raw doubles)
// distance + elapsed;                       // Error: no operator+ between Meters and Seconds

C++20 extends this pattern cleanly. Add the spaceship operator and arithmetic operators with requires constraints so only same-tag values combine:

cpp
// C++20
template<typename Tag, typename T>
struct StrongType {
    explicit StrongType(T v) : value(v) {}
    T value;

    auto operator<=>(const StrongType&) const = default;

    StrongType operator+(StrongType rhs) const
        requires std::is_arithmetic_v<T>
    { return StrongType{value + rhs.value}; }
};

A forward declaration is enough for the tag. The struct MetersTag; declaration inside the using line is a common shorthand:

cpp
// C++11
using Meters  = StrongType<struct MetersTag,  double>;  // MetersTag declared inline
using Seconds = StrongType<struct SecondsTag, double>;

Library alternatives include boost::units for full dimensional analysis and the type_safe library (https://github.com/foonathan/type_safe) as a header-only drop-in for strong typedefs.


std::type_identity_t<T> for Deduction Blocking

std::type_identity<T> (C++20, <type_traits>) is a struct whose only member is using type = T. Because the type appears inside a struct, it forms a non-deduced context: the compiler will not try to deduce T from that argument position.

cpp
// C++20
#include <type_traits>

template<typename T>
void scale(T x, std::type_identity_t<T> factor) {
    // factor's type is not used for deduction β€” T comes from x alone
}

scale(3.14f, 2);    // OK: T=float, factor implicitly converts int→float
scale(3.14f, 2.0);  // OK: T=float, factor is float
// scale(3.14f, 2L); // Error only if long→float conversion is lossy in your rules

Without type_identity_t, scale(3.14f, 2) would fail because T would be deduced as both float and int simultaneously.

Pre-C++20 workaround. Write the wrapper yourself β€” it is a one-liner:

cpp
// C++11
template<typename T> struct type_identity          { using type = T; };
template<typename T> using  type_identity_t = typename type_identity<T>::type;

Additional use: suppressing CTAD. Wrapping a constructor argument in type_identity_t prevents class template argument deduction from firing when you want explicit control over the instantiated type.

cpp
// C++20
// Force T to be explicitly provided rather than deduced from the argument
template<typename T>
auto make_box(std::type_identity_t<T> val) {
    return Box<T>{val};
}

auto b = make_box<float>(42);  // T=float, even though 42 is int

Alias Templates in Metaprogramming: Extracting Nested Types

The C++ standard library's type trait _t and _v aliases are alias templates over ::type and ::value members. Every std::remove_cv_t<T> you write expands to typename std::remove_cv<T>::type β€” the alias hides the typename boilerplate.

cpp
// C++14: _t aliases introduced (they exist as helpers since C++11 with typename)
std::remove_cv_t<const int>              // β†’ int
std::decay_t<int(&)[4]>                  // β†’ int*
std::result_of_t<Callable(int, double)>  // β†’ return type of Callable (deprecated C++17)
std::invoke_result_t<Callable, int>      // β†’ C++17 replacement

Writing your own extractor. Any associated type a class exposes as a nested alias can be wrapped:

cpp
// C++11
template<typename T> using element_type_t  = typename T::element_type;
template<typename T> using value_type_t    = typename T::value_type;
template<typename T> using difference_type_t = typename T::difference_type;

Conditional alias. std::conditional_t itself is just:

cpp
// C++14
template<bool B, typename T, typename F>
using conditional_t = typename std::conditional<B, T, F>::type;

You can build the same pattern for your own dispatch tables.

Detection idiom (pre-C++20). Before concepts, a SFINAE-based traits struct combined with an alias variable served as a type capability probe:

cpp
// C++17
template<typename T, typename = void>
struct has_value_type : std::false_type {};

template<typename T>
struct has_value_type<T, std::void_t<typename T::value_type>> : std::true_type {};

template<typename T>
inline constexpr bool has_value_type_v = has_value_type<T>::value;

C++20 concepts subsume many detection patterns, but alias templates remain the composition primitive β€” concepts are often defined in terms of alias template expressions.


decltype-Based Aliases for Computed Types

decltype(expr) yields the type of an expression without evaluating it. Combined with std::declval<T>(), which produces a value of type T in an unevaluated context, you can derive the result type of any operation at compile time and name it with using.

cpp
// C++11
template<typename A, typename B>
using SumType = decltype(std::declval<A>() + std::declval<B>());

SumType<int, double>   x;  // double
SumType<float, float>  y;  // float

This is the mechanism behind std::common_type_t<A, B> (C++11) and std::common_reference_t<A, B> (C++20), which are both library aliases computed from decltype expressions.

Static size computed from a type. A using alias can embed any constant expression that the type provides:

cpp
// C++11
template<typename T>
using BitArrayOf = std::array<bool, sizeof(T) * 8>;

BitArrayOf<uint32_t> flags;  // std::array<bool, 32>
BitArrayOf<uint64_t> mask;   // std::array<bool, 64>

Trailing return type aliases. Before C++14 deduced return types, decltype trailing returns were the idiomatic way to express forwarding functions:

cpp
// C++11
template<typename F, typename... Args>
auto call(F&& f, Args&&... args)
    -> decltype(std::forward<F>(f)(std::forward<Args>(args)...))
{
    return std::forward<F>(f)(std::forward<Args>(args)...);
}

C++20 abbreviated function templates. auto f(auto x) introduces an implicit template parameter for each auto. The deduced type can be constrained inline with concepts, making decltype-based aliases less necessary at function signatures β€” though they remain essential inside generic lambdas and metaprogramming machinery.

cpp
// C++20
auto add(auto a, auto b) -> decltype(a + b) {
    return a + b;
}

See Also

  • Templates β€” alias templates require understanding of template declaration syntax
  • CTAD β€” C++17 class template argument deduction and C++20 alias template CTAD
  • CRTP β€” alias templates reduce CRTP boilerplate in policy hierarchies
  • auto β€” auto and alias templates are complementary tools for reducing type verbosity