Lluís Alemany-Puig
Lluís Alemany-Puig

Reputation: 1243

Generalizing std::conditional_t<>

I have a function that computes a certain object from a given parameter (say, an important node from a graph). Now, when calculating such an object, the function might allocate some memory. Sometimes I want the function to just return the result, and sometimes to return the result plus the memory used to compute it.

I typically solve this binary case like this:

enum class what {
    what1,  // return, e.g., just an int
    what2   // return, e.g., a std::pair<int, std::vector<int>>
};

template <what w>
std::conditional_t<w == what::what1, int, std::pair<int, std::vector<int>>>
calculate_something(const param& p) { ... }

I would like to generalize the solution above to longer enumerations

enum class list_whats {
    what1,
    what2,
    what3,
    what4,
    what5
};

One possible solution is to nest as many std::conditional_t as needed

template <list_whats what>
std::conditional_t<
    what == list_whats::what1,
    int,
    std::conditional_t<
        what == list_whats::what2,
        float,
        ....
    >
>
>
calculate_something(const param& p) { ... }

But this is cumbersome and perhaps not too elegant.

Does anyone know how to do this in C++ 17?

EDIT

To make the question perfectly clear: how do I implement the function return_something so as to be able to run the following main?

int main() {
    int s1 = return_something<list_whats::what1>();
    s1 = 3;

    float s2 = return_something<list_whats::what2>();
    s2 = 4.0f;

    double s3 = return_something<list_whats::what3>();
    s3 = 9.0;

    std::string s4 = return_something<list_whats::what4>();
    s4 = "qwer";

    std::vector<int> s5 = return_something<list_whats::what5>();
    s5[3] = 25;
}

Upvotes: 0

Views: 404

Answers (5)

Llu&#237;s Alemany-Puig
Llu&#237;s Alemany-Puig

Reputation: 1243

Although I already posted an answer to my own question, and I accepted an answer from another user, I thought I could post another possibility in tackling this problem using the following struct

template <typename... Ts> struct type_sequence { };

which I learnt about in this talk by Andrei Alexandrescu. Since I learnt quite a bit by using it and the result is a bit simpler than the original answer that used two nested structs I thought I would share it here. However, the solution I would actually implement is the one that was accepted.

This is the full code with a main function included. Notice the change in the specification of function return_something. Now this function indicates the return type (which I like very much, perhaps I'm old fashioned) in a more readable way than in my first answer. You can try it out here.

#include <type_traits>
#include <iostream>
#include <vector>

template <bool... values>
struct which {
    template <std::size_t idx, bool v1, bool... vs>
    struct _which_impl {
        static constexpr std::size_t value =
            (v1 ? idx : _which_impl<idx + 1, vs...>::value);
    };

    template <std::size_t idx, bool v>
    struct _which_impl<idx, v> {
        static constexpr std::size_t value = (v ? idx : idx + 1);
    };

    static constexpr std::size_t value = _which_impl<0, values...>::value;
};

template <std::size_t ith_idx, typename... Ts>
struct ith_type {
    template <std::size_t cur_idx, typename t1, typename... ts>
    struct _ith_type_impl {
        using type =
            std::conditional_t<
                ith_idx == cur_idx,
                t1,
                typename _ith_type_impl<cur_idx + 1, ts...>::type
            >;
    };

    template <std::size_t cur_idx, typename t1>
    struct _ith_type_impl<cur_idx, t1> {
        using type = std::conditional_t<ith_idx == cur_idx, t1, std::nullptr_t>;
    };

    using type = typename _ith_type_impl<0, Ts...>::type;
};

template <std::size_t ith_idx, typename... ts>
using ith_type_t = typename ith_type<ith_idx, ts...>::type;

template <bool... conds>
static constexpr std::size_t which_v = which<conds...>::value;

template <typename... Ts> struct type_sequence { };

template <bool... values> struct bool_sequence {
    static constexpr std::size_t which = which_v<values...>;
};

template <std::size_t ith_idx, typename... Ts>
struct ith_type<ith_idx, type_sequence<Ts...>> : ith_type<ith_idx, Ts...>
{ };

template <typename bool_sequence, typename type_sequence>
struct conditional_list {
    using type = ith_type_t<bool_sequence::which, type_sequence>;
};

template <typename bool_sequence, typename type_sequence>
using conditional_list_t =
    typename conditional_list<bool_sequence, type_sequence>::type;

enum class list_whats {
    what1,
    what2,
    what3,
    what4,
    what5,
};

template <list_whats what>
conditional_list_t<
    bool_sequence<
        what == list_whats::what1,
        what == list_whats::what2,
        what == list_whats::what3,
        what == list_whats::what4,
        what == list_whats::what5
    >,
    type_sequence<
        int,
        float,
        double,
        std::string,
        std::vector<int>
    >
>
return_something() noexcept {
    if constexpr (what == list_whats::what1) { return 1; }
    if constexpr (what == list_whats::what2) { return 2.0f; }
    if constexpr (what == list_whats::what3) { return 3.0; }
    if constexpr (what == list_whats::what4) { return "42"; }
    if constexpr (what == list_whats::what5) { return {1,2,3,4,5}; }
}

int main() {
    [[maybe_unused]] auto s1 = return_something<list_whats::what1>();
    [[maybe_unused]] auto s2 = return_something<list_whats::what2>();
    [[maybe_unused]] auto s3 = return_something<list_whats::what3>();
    [[maybe_unused]] auto s4 = return_something<list_whats::what4>();
    [[maybe_unused]] auto s5 = return_something<list_whats::what5>();
}

Upvotes: 0

fdan
fdan

Reputation: 1804

I don't think you should use std::conditional at all to solve your problem. If I get this right, you want to use a template parameter to tell your function what to return. The elegant way to do this could look something like this:

#include <vector>

enum class what { what1, what2 };

template <what W>
auto compute() {
  if constexpr (W == what::what1) {
    return 100;
  }
  if constexpr (W == what::what2) {
    return std::pair{100, std::vector<int>{}};
  }
}

auto main() -> int {
  [[maybe_unused]] const auto as_int = compute<what::what1>();
  [[maybe_unused]] const auto as_pair = compute<what::what2>();
}

You can also use template specialization if you prefer another syntax:

template <what W>
auto compute();

template <>
auto compute<what::what1>() {
  return 100;
}

template <>
auto compute<what::what2>() {
  return std::pair{100, std::vector<int>{}};
}

Upvotes: 2

Stack Danny
Stack Danny

Reputation: 8126

Here's my approach:

  • what_pair is a pair that corresponds one enum to one type.

  • what_type_index accepts a enum and a std::tuple<what_pair<...>...> and searches the tuple map where the enums are equal and returns index. It returns maximum std::size_t value, if no match was found.

  • what_type is the final type, it is the tuple element at the found position. The program won't compile when the index is std::size_t max value because of invalid std::tuple access.

template<what W, typename T>
struct what_pair {
    constexpr static what w = W;
    using type = T;
};

template<what w, typename tuple_map>
constexpr auto what_type_index() {
    std::size_t index = std::numeric_limits<std::size_t>::max();
    auto search_map = [&]<std::size_t... Ints>(std::index_sequence<Ints...>) {
        ((std::tuple_element_t<Ints, tuple_map>::w == w ? (index = Ints) : 0), ...);
    };
    search_map(std::make_index_sequence<std::tuple_size_v<tuple_map>>());
    return index;
}

template<what w, typename tuple_map>
using what_type = typename 
    std::tuple_element_t<what_type_index<w, tuple_map>(), tuple_map>::type;

and this is the example usage:

int main() {
    using what_map = std::tuple<
        what_pair<what::what1, int>,
        what_pair<what::what2, float>,
        what_pair<what::what3, double>,
        what_pair<what::what4, std::string>,
        what_pair<what::what5, std::vector<int>>>;

    static_assert(std::is_same_v<what_type<what::what1, what_map>, int>);
    static_assert(std::is_same_v<what_type<what::what2, what_map>, float>);
    static_assert(std::is_same_v<what_type<what::what3, what_map>, double>);
    static_assert(std::is_same_v<what_type<what::what4, what_map>, std::string>);
    static_assert(std::is_same_v<what_type<what::what5, what_map>, std::vector<int>>);

    //compilation error, because 'what6' wasn't specified in the 'what_map'
    using error = what_type<what::what6, what_map>;
}

try it out on godbolt.

Upvotes: 1

lorro
lorro

Reputation: 10880

An alternative to working only with types is to write a function that returns the specific type, or an identity<type>. It's sometimes more readable. Here is an example:

    // if you don't have it in std
    template<typename T>
    struct identity {
        using type = T;
    };
     
    enum class what {
        what1,
        what2,
        what3
    };
     
    template<what w>
    auto return_type_for_calc() {
        if constexpr (w == what::what1) {
            return identity<int>();
        } else if constexpr (w==what::what2) {
            return identity<double>();
        } else {
            return identity<float>();
        }
    }
     
    template<what w>
    decltype(return_type_for_calc<w>())
    calculate_something()
    {
        return {};
    }
     
    int main() {
        calculate_something<what::what1>();
        calculate_something<what::what2>();
        return 0;
    }

Upvotes: 0

Llu&#237;s Alemany-Puig
Llu&#237;s Alemany-Puig

Reputation: 1243

I found a possible solution that nests two structs: the first takes a list of Boolean values to indicate which type should be used, and the nested struct takes the list of possible types (see conditional_list in the example code below -- the nested structs were inspired by this answer). But perhaps it's not elegant enough. I'm wondering if there is a possible solution of the form

std::conditional_list<
    ..., // list of Boolean values, (of any length!)
    ... // list of types           (list that should be as long as the first)
>::type

My proposal

#include <type_traits>
#include <iostream>
#include <vector>

// -----------------------------------------------------------------------------

template<auto A, auto... ARGS>
constexpr auto sum = A + sum<ARGS...>;
template<auto A>
constexpr auto sum<A> = A;

// -----------------------------------------------------------------------------

template <bool... values>
static constexpr bool exactly_one_v = sum<values...> == 1;

// -----------------------------------------------------------------------------

template <bool... values>
struct which {
    static_assert(exactly_one_v<values...>);

    template <std::size_t idx, bool v1, bool... vs>
    struct _which_impl {
        static constexpr std::size_t value =
            (v1 ? idx : _which_impl<idx + 1, vs...>::value);
    };

    template <std::size_t idx, bool v>
    struct _which_impl<idx, v> {
        static constexpr std::size_t value = (v ? idx : idx + 1);
    };

    static constexpr std::size_t value = _which_impl<0, values...>::value;
};

template <bool... conds>
static constexpr std::size_t which_v = which<conds...>::value;

// -----------------------------------------------------------------------------

template <std::size_t ith_idx, typename... Ts>
struct ith_type {
    template <std::size_t cur_idx, typename t1, typename... ts>
    struct _ith_type_impl {
        typedef
            std::conditional_t<
                ith_idx == cur_idx,
                t1,
                typename _ith_type_impl<cur_idx + 1, ts...>::type
            >
            type;
    };

    template <std::size_t cur_idx, typename t1>
    struct _ith_type_impl<cur_idx, t1> {
        typedef
            std::conditional_t<ith_idx == cur_idx, t1, std::nullptr_t>
            type;
    };

    static_assert(ith_idx < sizeof...(Ts));
    typedef typename _ith_type_impl<0, Ts...>::type type;
};

template <std::size_t ith_idx, typename... ts>
using ith_type_t = typename ith_type<ith_idx, ts...>::type;

// -----------------------------------------------------------------------------

template <bool... conds>
struct conditional_list {
    template <typename... ts>
    struct good_type {
        static_assert(sizeof...(conds) == sizeof...(ts));
        typedef ith_type_t<which_v<conds...>, ts...> type;
    };
};

// -----------------------------------------------------------------------------

enum class list_whats {
    what1,
    what2,
    what3,
    what4,
    what5,
};

template <list_whats what>
typename conditional_list<
    what == list_whats::what1,
    what == list_whats::what2,
    what == list_whats::what3,
    what == list_whats::what4,
    what == list_whats::what5
>::template good_type<
    int,
    float,
    double,
    std::string,
    std::vector<int>
>::type
return_something() noexcept {
    if constexpr (what == list_whats::what1) { return 1; }
    if constexpr (what == list_whats::what2) { return 2.0f; }
    if constexpr (what == list_whats::what3) { return 3.0; }
    if constexpr (what == list_whats::what4) { return "42"; }
    if constexpr (what == list_whats::what5) { return {1,2,3,4,5}; }
}

int main() {
    auto s1 = return_something<list_whats::what1>();
    s1 = 3;

    auto s2 = return_something<list_whats::what2>();
    s2 = 4.0f;

    auto s3 = return_something<list_whats::what3>();
    s3 = 9.0;

    auto s4 = return_something<list_whats::what4>();
    s4 = "qwer";

    auto s5 = return_something<list_whats::what5>();
    s5[3] = 25;
}

Upvotes: 0

Related Questions