P2527R1
std::variant_alternative_index and std::tuple_element_index

Published Proposal,

This version:
http://wg21.link/P2527R1
Author:
(Apple)
Audience:
SG18
Project:
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
Source:
https://github.com/achristensen07/papers/blob/master/source/P2527r1.bs

Abstract

A description of std::variant_alternative_index, std::variant_alternative_index_v, std::tuple_element_index, and std::tuple_element_index_v which polish use of std::variant and std::tuple in C++.

1. Introduction

std::variant and std::tuple seem have a missing piece. std::get and std::get_if can be used with an index or with a type. There is already a way to get a type from an index (std::variant_alternative and std::tuple_element) but there is no way to get an index from a type. This adds such a mechanism.

2. Revision History

3. Motivating Use Case: Migrating C code to use std::variant

Consider the following C program with a unit test:

#include <assert.h>

struct NumberStorage {
    enum Type { TYPE_INT, TYPE_DOUBLE } type;
    union {
        int i;
        double d;
    };
};

struct NumberStorage packageInteger(int i) {
    struct NumberStorage packaged;
    packaged.type = TYPE_INT;
    packaged.i = i;
    return packaged;
}

int main() {
    struct NumberStorage i = packageInteger(5);
    assert(i.type == TYPE_INT);
}

In order to migrate it from using a union to using a std::variant one of the cleanest solutions looks something like this:

#include <assert.h>
#include <variant>

// Dear future developers: VariantIndex and NumberStorage must stay in sync.
// If you reorder or add to one, you must do the same to the other.
enum class VariantIndex : std::size_t { Int, Double };
using NumberStorage = std::variant<int, double>;

NumberStorage packageInteger(int i) {
    return { i };
}

int main() {
    auto i = packageInteger(5);
    auto expectedIndex = static_cast<std::size_t>(VariantIndex::Int);
    assert(i.index() == expectedIndex);
}

Instead, using std::variant_alternative_index_v would make the code look cleaner and be easier to maintain:

#include <assert.h>
#include <variant>

using NumberStorage = std::variant<int, double>;

NumberStorage packageInteger(int i) {
    return { i };
}

int main() {
    auto i = packageInteger(5);
    auto expectedIndex = std::variant_alternative_index_v<int, NumberStorage>;
    assert(i.index() == expectedIndex);
}

4. Motivating Use Case: Simple object serialization

Another place where std::variant_alternative_index_v is useful is when we have an index from a source such as deserialization and we want to decide what type it represents without using any magic numbers:

struct Reset { };
struct Close { };
struct RunCommand { std::string command; };

using Action = std::variant<Reset, Close, RunCommand>;

void serializeAction(const Action& action, std::vector<uint8_t>& buffer)
{
    buffer.push_back(action.index());
    if (auto* runCommand = std::get_if<RunCommand>(&action))
        serializeString(runCommand->command, buffer);
}

std::optional<Action> deserializeAction(std::span<const uint8_t> source)
{
    if (!source.size())
        return std::nullopt;

    switch (source[0]) {
    case std::variant_alternative_index_v<Reset, Action>:
        return Reset { };
    case std::variant_alternative_index_v<Close, Action>:
        return Close { };
    case std::variant_alternative_index_v<RunCommand, Action>:
        return RunCommand { deserializeString(source.subspan(1)) };
    }

    return std::nullopt;
}

Like the other motivating use case, this could be done with an enum class or #define RESET_INDEX 0 etc., but this is nicer and references the variant instead of requiring parallel metadata. This was the use case that motivated an implementation in WebKit.

5. Motivation For std::tuple_element_index_v

Feedback from r0 indicated that while adding std::variant_alternative_index_v for variants, it would be symmetric to add std::tuple_element_index_v for tuples.

6. Addition to variant.helper section

template<class T, class Variant> struct variant_alternative_index;

All specializations of variant_alternative_index meet the Cpp17UnaryTypeTrait requirements (21.3.2) with a base characteristic of integral_constant<size_t, N> for some N.

template<class T, class Variant> struct variant_alternative_index<T, const Variant>;

Let VAI denote variant_alternative_index<T, Variant> of the cv-unqualified type T. Then each specialization of the template meets the Cpp17UnaryTypeTrait requirements (21.3.2) with a base characteristic of integral_constant<size_t, VAI::value>.

template<class T, class... Types> struct variant_alternative_index<T, variant<Types...>>
    : integral_constant<size_t, N> { };

Mandates: The type T occurs exactly once in Types.
N is the zero-based index of T in Types.

template<class Type, class Variant> inline constexpr size_t variant_alternative_index_v
    = variant_alternative_index<Type, Variant>::value;

7. Addition to tuple.helper section

template<class T, class Tuple> struct tuple_element_index;

All specializations of tuple_element_index meet the Cpp17UnaryTypeTrait requirements (21.3.2) with a base characteristic of integral_constant<size_t, N> for some N.

template<class T, class Tuple> struct tuple_element_index<T, const Tuple>;

Let TEI denote tuple_element_index<T, Tuple> of the cv-unqualified type T. Then each specialization of the template meets the Cpp17UnaryTypeTrait requirements (21.3.2) with a base characteristic of integral_constant<size_t, TEI::value>.

template<class T, class... Types> struct tuple_element_index<T, tuple<Types...>>
    : integral_constant<size_t, N> { };

Mandates: The type T occurs exactly once in Types.
N is the zero-based index of T in Types.

template<class Type, class Tuple> inline constexpr size_t tuple_element_index_v
    = tuple_element_index<Type, Tuple>::value;

8. Ill-formed use

Like the versions of std::get that take a type, the program must be ill formed if the type is not a unique element in the types of the std::variant or std::tuple such as in these four cases:

using Example1 = std::variant<int, double, double>;
auto ambiguous1 = std::variant_alternative_index_v<double, Example1>;

using Example2 = std::variant<int, double>;
auto missing2 = std::variant_alternative_index_v<float, Example2>;

using Example3 = std::tuple<int, double, double>;
auto ambiguous3 = std::tuple_element_index_v<double, Example3>;

using Example4 = std::tuple<int, double>;
auto missing4 = std::tuple_element_index_v<float, Example4>;

If the constness of the searched-for type does not match the constness of the type in the std::variant or std::tuple then there should be a compiler error. This is also the case with std::get.

using Example5 = std::variant<int, double>;
auto constDoesNotMatch5 = std::variant_alternative_index_v<const int, Example5>;

using Example6 = std::tuple<int, double>;
auto constDoesNotMatch6 = std::tuple_element_index_v<const int, Example6>;

Like std::variant_size and std::tuple_size there should be a compiler error if std::variant_alternative_index or std::tuple_element_index_v are used with classes or structs that are not std::variants or std::tuples, respectively, including classes or structs that inherit from std::variant or std::tuple.

class Example7 : public std::variant<int, double> { };
auto nonVariantParameter7 = std::variant_alternative_index_v<int, Example7>;
auto nonVariantParameter8 = std::variant_altermative_index_v<int Example4>;

class Example9 : public std::tuple<int, double> { };
auto nonTupleParameter9 = std::tuple_element_index_v<int, Example9>;
auto nonTupleParameter10 = std::tuple_element_index_v<int, Example2>;

9. Possible implementation

namespace std {

namespace detail {

template<size_t, class, class> struct alternative_index_helper;

template<size_t index, class Type, class T>
struct alternative_index_helper<index, Type, variant<T>> {
    static constexpr size_t count = is_same_v<Type, T>;
    static constexpr size_t value = index;
};

template<size_t index, class Type, class T, class... Types>
struct alternative_index_helper<index, Type, variant<T, Types...>> {
    static constexpr size_t count = is_same_v<Type, T> + alternative_index_helper<index + 1, Type, variant<Types...>>::count;
    static constexpr size_t value = is_same_v<Type, T> ? index : alternative_index_helper<index + 1, Type, variant<Types...>>::value;
};

template<size_t, class, class> struct tuple_element_helper;

template<size_t index, class Type, class T>
struct tuple_element_helper<index, Type, tuple<T>> {
    static constexpr size_t count = is_same_v<Type, T>;
    static constexpr size_t value = index;
};

template<size_t index, class Type, class T, class... Types>
struct tuple_element_helper<index, Type, tuple<T, Types...>> {
    static constexpr size_t count = is_same_v<Type, T> + tuple_element_helper<index + 1, Type, tuple<Types...>>::count;
    static constexpr size_t value = is_same_v<Type, T> ? index : tuple_element_helper<index + 1, Type, tuple<Types...>>::value;
};

} // namespace detail

template<class T, class Variant> struct variant_alternative_index;

template<class T, class Variant> struct variant_alternative_index<T, const Variant>
    : variant_alternative_index<T, Variant> { };

template<class T, class... Types> struct variant_alternative_index<T, variant<Types...>>
    : integral_constant<size_t, detail::alternative_index_helper<0, T, variant<Types...>>::value> {
    static_assert(detail::alternative_index_helper<0, T, remove_cv_t<variant<Types...>>>::count == 1);
};

template<class T, class Variant> inline constexpr size_t variant_alternative_index_v = variant_alternative_index<T, Variant>::value;

template<class T, class Tuple> struct tuple_element_index;

template<class T, class Tuple> struct tuple_element_index<T, const Tuple> : tuple_element_index<T, Tuple> { };

template<class T, class Tuple> struct tuple_element_index
    : integral_constant<size_t, detail::tuple_element_helper<0, T, remove_const_t<Tuple>>::value> {
    static_assert(detail::tuple_element_helper<0, T, remove_cv_t<Tuple>>::count == 1);
};

template<class T, class Tuple> inline constexpr size_t tuple_element_index_v = tuple_element_index<T, Tuple>::value;

} // namespace std