Two kinds of tag types: foo_t and foo_tag

In C++, when we have a type that carries no data — whose only “identity” is its type — we conventionally call that a “tag type.” When you see code like struct X {}; it’s often the case that X is a tag type. However, not all tag types are created equal: there are at least two major disjoint use-cases for tag types, and the STL (as of 2025) therefore uses two distinct naming conventions for their identifiers.

There is no widely recognized nomenclature for these two kinds of tag types, as far as I know, so I’m going to call them disambiguation tags and concept tags.

Disambiguation tag types

A disambiguation tag type looks like this:

struct foo_t {}; // maybe with an explicit ctor
inline constexpr foo_t foo = foo_t();

A disambiguation tag is passed explicitly by the caller to select among overloads of a library function — typically, a constructor. (Because otherwise we could use the function name itself to disambiguate; but all constructors belong to the same overload set by definition. See my C++Now 2021 talk “When should you give two things the same name?”) So we end up with code like this:

struct useful_class {
  explicit useful_class(foo_t, int, int);
  explicit useful_class(bar_t, int, int);
};

auto myVar = useful_class(foo, 1, 2);

In this pattern the tag’s name (sans _t) denotes a verb or prepositional phrase, or some kind of description of the functionality you’re asking for — a description that we would have put into the name of the function, except that we can’t, because the function is a constructor. Examples from the STL include:

All of the above are passed only to constructors. std::nothrow_t is passed to operator new. std::destroying_delete_t is passed to operator delete.

Concept tag types

A concept tag type looks like this instead:

struct foo_tag {};

template<class T> concept foo = ~~~~;

A concept tag is never named explicitly at the call-site. It is used in generic programming — that is, inside a template that doesn’t know quite all the capabilities of the type it’s received. So the template will dispatch on a member typedef with a name conventionally ending in _category or _concept, which the user-supplied type must provide. Like this:

template<class T> void internal_algorithm(T, foo_tag);
template<class T> void internal_algorithm(T, bar_tag);

template<class T>
void useful_template(T t) {
  internal_algorithm(t, typename T::your_category());
}

struct MyFoolikeClass {
  using your_category = foo_tag;
};

In post-C++20 code, this pattern can often be replaced entirely with concept overloading or simply with if constexpr:

template<class T> void internal_algorithm_for_foos(T);
template<class T> void internal_algorithm_for_bars(T);

template<class T>
void useful_template(T t) {
  if constexpr (foo<T>) {
    internal_algorithm_for_foos(t);
  } else {
    static_assert(bar<T>);
    internal_algorithm_for_bars(t);
  }
}

You can combine this with inheritance, to build a class hierarchy of concept tags corresponding to your concept hierarchy of concepts. Then my user-supplied class U should (or is assumed to) model your concept foo exactly when U::your_category is — or is derived fromfoo_tag. The STL does it:

struct forward_iterator_tag : input_iterator_tag {};

template<class T>
concept forward_iterator = input_iterator<T> && ~~~~;

Of course the STL’s iterator machinery long predates concepts, and when Concepts arrived in C++20 there was some retrofitting involved; so in fact the STL has member typedefs named both U::iterator_category and U::iterator_concept, and (since C++20) ordinary users never need to supply either of them.

In this pattern the tag’s name (sans _tag) always denotes a concept — a category of behaviors that the user-supplied type has — and the member typedef name always ends in _category or _concept. The C++23 STL has only one family of examples:

  • iterator_category and iterator_concept (input_iterator_tag, output_iterator_tag, forward_iterator_tag, etc.)

But Boost has many more:

  • directed_category (directed_tag, undirected_tag, etc.) in Boost.Graph
  • traversal_category (vertex_list_graph_tag, etc.) in Boost.Graph
  • fusion_tag (deque_tag, vector_tag, etc.) in Boost.Fusion
  • stepper_category (stepper_tag, etc.) in Boost.Numeric.Odeint
  • orientation_category (row_major_tag, column_major_tag, etc.) in Boost.uBlas
  • type_category (tensor_tag, etc.) in Boost.uBlas
  • storage_category (dense_tag, packed_tag, sparse_tag, etc.) in Boost.uBlas

Differences between the two kinds

Disambiguation tags Concept tags
Ends with _t Ends with _tag
Without the suffix it’s an inline constexpr variable Without the suffix it’s a concept
Used in concrete programming Used only in generic programming
No associated member typedef Associated member typedef ending with _category or _concept
Named in the caller Named in the typedef definition, and in the callee
Callee is usually a constructor Callee is generic-programming machinery
Never derive from each other May derive from each other to form a hierarchy

Can we have nice things?

This being C++, of course we can’t. There are at least two awkward bits of trivia in this area which will arrive with C++26.

Kind-blurring in Boost.uBlas and in C++26 <linalg>

As noted above, Boost.uBlas deals with row/column-major storage order by providing a family of concept tags: Boost defines row_major_tag and column_major_tag, and the user instantiates matrix<int, L> where typename L::orientation_category is expected to be one of those two tags. Boost also provides a type named row_major such that row_major::orientation_category is row_major_tag; so that matrix<int, row_major> Just Works. Note that the unsuffixed row_major denotes a class type, not a concept; in all other respects, I’d say that Boost.uBlas follows the “concept tag” pattern closely.

The BLAS wrapper just adopted for C++26, P1673 <linalg>, deals with storage order by giving std::mdspan a LayoutPolicy parameter. At the top level, layout_right means row-major and layout_left means column-major. But you can also give it a layout that says “I store only the top- or bottom-triangular portion of a matrix,” which is spelled by instantiating std::linalg::layout_blas_packed with some tag types. At this level row-major-ness is no longer spelled layout_right; now it’s spelled row_major_t.

struct row_major_t {
  explicit row_major_t() = default;
};
inline constexpr row_major_t row_major{};

If I understand correctly, the user will write something like this:

namespace la = std::linalg;
using MyLayout = la::layout_blas_packed<la::upper_triangle_t, la::row_major_t>;
std::mdspan<int, std::dextents<size_t, 2>, MyLayout> m = ~~~~;
using M = decltype(m);

In this example, M::layout_type::storage_order_type is linalg::row_major_t. Various bits of generic <linalg> machinery will dispatch on storage_order_type.

So <linalg> starts out conforming to the “disambiguation tag” pattern: there’s a _t tag with an associated inline constexpr variable. But it seems to me that the inline constexpr variable is never used. Meanwhile, the type is used as if it were a “concept tag,” with an associated member typedef… but that typedef is named storage_order_type instead of (as we’d expect) storage_order_category.

To further confuse matters, linalg::upper_triangle_t works the same way — as something like a concept tag — when it’s inside a layout; but it is also used in the public-facing API as a disambiguation tag. These two function calls perform different operations:

la::triangular_matrix_matrix_right_solve(A, la::upper_triangle, la::explicit_diagonal, B);
la::triangular_matrix_matrix_right_solve(A, la::lower_triangle, la::explicit_diagonal, B);

Here there’s no physical reason to use disambiguation tags instead of distinct function names: the API could have been designed with separate triangular_matrix_matrix_right_solve_upper and triangular_matrix_matrix_right_solve_lower algorithms. But that leads to a combinatorial explosion of function names, especially when you apply the same rationale to the next parameter, which can be la::explicit_diagonal or la::implicit_unit_diagonal (but nothing else).

The same API decision appears in C++23 flat_set: it spells fs.insert(std::sorted_unique, il) with a disambiguation tag rather than invent a new function name such as fs.insert_sorted_unique(il).

That is, P1673’s row_major and column_major are apparently used only as concept tags (with odd names and apparently vestigial inline constexpr variables); explicit_diagonal and implicit_unit_diagonal are used only as disambiguation tags; upper_triangle and lower_triangle are used as both.

Wrong-naming in C++26 <execution>

The <execution> header, a.k.a. P2300 “Senders and Receivers” (S&R), adds four new “concept tags,” following the pattern precisely to the letter, except that (as of November 2025) it misnames them using _t rather than _tag. In each case we have the tag type and its associated concept:

struct sender_t {};

template<class Sndr>
  concept sender = ~~~~;

User code (e.g. this example from [exec.cmplsig]/2) has a member typedef ending in _concept:

struct my_sender {
  using sender_concept = sender_t;
  ~~~~
};

This follows the “concept tag” pattern to the letter… except for the naming of sender_t!

Property of sender_t Disambiguation
tag-like
Concept
tag-like
Ends with _t
Without the suffix it’s a concept
Used only in generic programming
Associated member typedef ending with _concept
Named in the typedef definition, and in the callee
Callee is generic-programming machinery
Don’t derive from each other

So sender_t really “should” have been named sender_tag, as it has been (organically and independently, as far as I know) in jaredhoberock/croquet since 2019, janciesko/stdexx since June 2025, Cra3z/coio since August 2025, and maybe a few other places.

In an ideal world, the following four “concept tag” types would be renamed before finalizing C++26:

Postscript: S&R query objects

Be aware that <execution> also provides more than three dozen inline constexpr variables in the std::execution namespace, which at first glance might look like disambiguation tags:

inline constexpr forwarding_query_t forwarding_query{};
inline constexpr get_allocator_t get_allocator{};
inline constexpr get_stop_token_t get_stop_token{};
[...]
inline constexpr into_variant_t into_variant{};
inline constexpr stopped_as_optional_t stopped_as_optional{};
inline constexpr stopped_as_error_t stopped_as_error{};
inline constexpr associate_t associate{};
inline constexpr spawn_future_t spawn_future{};

But in fact these aren’t tags; they’re a particular kind of CPO that S&R calls “query objects” — not necessarily empty of data members, and certainly possessing member functions. struct forwarding_query_t (uniquely out of the three dozen) is currently also suggested for use as a base class, like view_base and enable_shared_from_this; but there is an open LWG issue pointing out that this makes no sense and should probably be removed.

Posted 2025-12-03