C++ using vs typedef: Alias Templates, Function Pointers, and the const Trap

Key takeaways

typedef and using declare the same thing — another name for an existing type — but only using can be templated, and its name = type form is far easier to read for function pointers. Neither creates a new type, and a typedef'd pointer combined with const produces a const pointer, not a pointer to const.

Two spellings of the same idea

A type alias gives an existing type a second name. C++ inherited typedef from C, and C++11 added the alias-declaration using Name = Type;. For non-template aliases they are exactly equivalent — the standard defines an alias-declaration as having the same semantics as a typedef declaration.

typedef std::vector<int> IntVec;   // C / C++03
using IntVec = std::vector<int>;   // C++11

So why did C++11 add a second syntax? Two reasons, and they are the only things this comparison really comes down to:

  1. typedef cannot be templated. using can.
  2. typedef hides the new name in the middle of a declarator, which gets unreadable for function pointers and arrays. using always puts the name on the left.

Reason 1: alias templates

Before C++11 there was no way to write “std::vector with my allocator, for any T”. Trying it with typedef is a hard error:

template <typename T>
typedef std::vector<T> Vec;   // error: template declaration of 'typedef'

The C++03 workaround was a struct with a nested typedef, which forces every user to write typename ...::type:

template <typename T>
struct VecOld { typedef std::vector<T> type; };

template <typename T>
void useOld() {
    typename VecOld<T>::type v;   // 'typename' required: dependent name
}

With using, the alias itself is a template:

template <typename T>
using Vec = std::vector<T>;

template <typename T>
using StringMap = std::map<std::string, T>;

template <typename T>
void useNew() {
    Vec<T> v;                     // no typename, no ::type
}

This is exactly why C++14 added std::remove_reference_t<T>, std::enable_if_t<...>, and the other _t helpers: they are alias templates over the older ::type traits, and they remove a lot of typename noise from generic code.

Alias templates are also transparent to deduction. template <typename T> void take(Vec<T>); is the same as take(std::vector<T>), so take(std::vector<int>{}) deduces T = int. The flip side: an alias template cannot be specialized. If you need Vec<bool> to mean something different, you must specialize a class template and alias its nested type:

template <> using Vec<bool> = std::vector<char>;   // error: not allowed

The workaround is the same pattern the standard library uses for its _t helpers: put the specializations on a class template, then alias its nested type.

template <typename T> struct VecImpl       { using type = std::vector<T>; };
template <>           struct VecImpl<bool> { using type = std::vector<char>; };

template <typename T>
using Vec2 = typename VecImpl<T>::type;   // Vec2<bool> is std::vector<char>

There is a cost to this version: Vec2<T> is no longer transparent to deduction. template <typename T> void take(Vec2<T>); cannot deduce T from a std::vector<int> argument, because the compiler would have to invert VecImpl to work out which T produces that type, and it never does that (T appears only in a non-deduced context). The error is couldn't deduce template parameter 'T'. Keep the simple alias when callers need deduction, and use the class-template form only when the specialization is worth it.

Alias templates and class template argument deduction

C++17 class template argument deduction (CTAD) lets you write std::vector v{1, 2, 3};, but in C++17 it did not work through alias templates: Vec v{1, 2, 3}; was an error because an alias template is not a class template. C++20 (P1814) extended CTAD to aliases, so the same line deduces Vec<int>. Compiler support arrived at different times: GCC implemented it in version 10, while Clang only completed it much later, so if your project builds with several compilers, spell out the template argument (Vec<int> v{1, 2, 3};) until every toolchain you use accepts the short form.

Reason 2: readable declarators

With typedef, the new name sits where a variable name would go in a declaration. For simple types that is fine; for function pointers you have to read inside-out.

typedef int* (*OldFn)(int);    // OldFn: pointer to function(int) returning int*
using NewFn = int* (*)(int);   // same type, name on the left

static_assert(std::is_same_v<OldFn, NewFn>);

The difference grows with nesting. The classic example is a function that takes and returns a function pointer, like POSIX signal:

typedef void (*Handler)(int);
Handler install(int sig, Handler h);          // readable only thanks to the typedef

using Handler2 = void (*)(int);
auto install2(int sig, Handler2 h) -> Handler2;

Member function pointers and arrays follow the same pattern:

struct Widget { int size() const; };

using SizeFn = int (Widget::*)() const;   // pointer to const member function
using Row    = int[4];                    // array of 4 ints
typedef int RowOld[4];                    // same, name buried in the middle

For function pointer semantics in depth, see C++ function pointers. In new code, std::function<void(int)> or a template parameter is often a better choice than a raw pointer, because it also accepts lambdas with captures — but that is a decision about the type, not about the alias syntax.

The const pointer trap

This one catches people in code reviews and interviews alike. An alias is substituted as a whole type, not textually like a macro. So adding const in front of a pointer alias makes the pointer const, not the thing it points at:

typedef char* CharPtr;

void f(const CharPtr p) {
    *p = 'x';     // compiles: p is char* const, the chars are mutable
    // p = nullptr;  // error: the pointer itself is const
}

static_assert(std::is_same_v<const CharPtr, char* const>);

Anyone reading const CharPtr naturally expands it in their head to const char*, which is the opposite of what the compiler does. Switching to using does not change this — using CharPtr = char*; behaves identically. The fix is to not hide pointers behind aliases unless the pointer-ness is deliberately part of an opaque handle type, and to write const char* spelled out when you mean read-only data.

I have run into the consequence of this in C-style APIs that alias every pointer (typedef struct node* NodePtr;) and then declare parameters as const NodePtr. The author clearly intended “this function will not modify the node”, the compiler enforced “this function will not reseat its local copy of the pointer”, and functions that were documented as read-only happily wrote through the pointer. Nothing warns about it because the code is well-formed.

Aliases never create a new type

A frequent misconception is that using Meters = double; adds type safety. It does not. The alias and the original are the same type, so everything that accepts one accepts the other:

using Meters  = double;
using Seconds = double;

double speed(Meters m, Seconds s) { return m / s; }

Meters  distance = 100;
Seconds time     = 9.58;
speed(time, distance);   // compiles, silently wrong

The same applies to overloading: void log(Meters) and void log(Seconds) declare the same function twice. If you want the compiler to catch unit mix-ups, you need a distinct type — even something as small as struct Meters { double value; }; — or a units library. Aliases are for naming, not for enforcing.

That said, naming is valuable. The common legitimate uses:

  • Shortening long types at their use site: using SessionMap = std::unordered_map<SessionId, std::shared_ptr<Session>>;
  • Single point of change: using Clock = std::chrono::steady_clock; lets you switch clocks in one line.
  • Member types for generic code: containers expose value_type, size_type, iterator so that algorithms can write typename C::size_type without knowing the concrete container.
  • Platform differences: using NativeHandle = HANDLE; on Windows, using NativeHandle = int; on POSIX, behind an #ifdef.
template <typename T>
class Container {
public:
    using value_type = T;
    using size_type  = std::size_t;

    size_type size() const { return data_.size(); }
private:
    std::vector<value_type> data_;
};

template <typename C>
typename C::size_type count(const C& c) { return c.size(); }

The mistake I see more often than any syntax problem is over-aliasing: a header where every type has a project-specific name (IntList, StrMap, CbFn), so readers must jump to a definition to learn that IntList is just std::vector<int>. An alias pays for itself when the underlying type is long, likely to change, or carries domain meaning; aliasing std::vector<int> to save six characters usually costs more in reading time than it saves in typing.

Not the same thing: namespace aliases and using-declarations

The keyword using has several unrelated jobs, which confuses newcomers:

namespace vln = very_long_namespace_name;   // namespace alias (no 'using' at all)
using std::string;                          // using-declaration: brings one name into scope
using namespace std;                        // using-directive: brings all names into scope
using Callback = std::function<void(int)>;  // alias-declaration: the subject of this article

Only the last form is a type alias. For namespace rules and why using namespace in headers is a problem, see C++ namespaces.

Quick decision table

SituationUse
New C++11+ codeusing
Header shared with Ctypedef (C has no using)
Template aliasusing (only option)
Function / member function pointer typeusing for readability
Need a distinct type for overloading or safetyneither — a struct wrapper
Need a “specialized” aliasclass template + nested type, then alias it

Aliases have no runtime cost in either form; they disappear entirely after type checking.