Search code examples
c++c++20constexprtype-traits

Is there a clean way to make declvals for types with no default constructors?


consider this example:

template<typename T>
concept Iteratable = requires(T n) {
    n.begin();
    n.end();
};

namespace detail {
    template<Iteratable T>
    using subtype = std::decay_t<decltype(*(std::declval<T>().begin()))>;

    template<Iteratable T>
    constexpr auto deepest_subtype_recursive() {

        if constexpr (Iteratable<subtype<T>>) {
            return detail::deepest_subtype_recursive<subtype<T>>();
        }
        else {
            return subtype<T>{};
        }
    }
}

template<Iteratable T>
using deepest_subtype = decltype(detail::deepest_subtype_recursive<T>());

A recursive function that calls itself with the subtype, until the type in question is no longer iteratable, then that type is returned. What this enables, is to find the deepest container type at compile time. So e.g. vector<list<deque<int>>> becomes int:

int main() {

    using container_type = std::vector<std::list<std::deque<int>>>;
    using deepest = deepest_subtype<container_type>;

    static_assert(std::is_same_v<deepest, int>);
}

using this way of return the value, and finding out the type with decltype is something I do very often, as I can write code that has more room to breathe and doesn't rely on insanley nested std::conditional_ts.


this example breaks, once I apply it on a class that doesn't have a default constructor:

struct foo {
    foo(int) {}
};

int main() {

    using container_type = std::vector<std::list<std::deque<foo>>>;
    using deepest = deepest_subtype<container_type>;

    static_assert(std::is_same_v<deepest, foo>);
}

error C2512: 'foo': no appropriate default constructor available

on the line where it says return subtype<T>{};.

I also can't say return std::declval<subtype<T>>(); as that will give the error error C2338: static_assert failed: 'Calling declval is ill-formed, see N4892 [declval]/2.'


To fix this I had the following idea: Skip the constructor by declaring a pointer, and immediately derefence it. So basically making my own declval:

template<typename T>
constexpr auto declval() {
    using pointer = T*;
    return *(pointer{});
}

namespace detail {
    template<Iteratable T>
    using subtype = std::decay_t<decltype(*(std::declval<T>().begin()))>;

    template<Iteratable T>
    constexpr auto deepest_subtype_recursive() {

        if constexpr (Iteratable<subtype<T>>) {
            return detail::deepest_subtype_recursive<subtype<T>>();
        }
        else {
            return declval<subtype<T>>();
        }
    }
}

this works, but it doesn't seem like a very clean solution to me.


Is there a clean way to declval? like maybe there is a utility like that already, or a utility that fully constructs a passed object.

Or am I supposed to abandon returning values and rather make the logic with only types, therefore using statements?


Solution

  • You can return std::type_identity<T>{} (which is always default-constructible) and use decltype()::type to get the wrapped type.

    namespace detail {
        template<Iteratable T>
        using subtype = std::decay_t<decltype(*(std::declval<T>().begin()))>;
    
        template<Iteratable T>
        constexpr auto deepest_subtype_recursive() {
            if constexpr (Iteratable<subtype<T>>) {
                return detail::deepest_subtype_recursive<subtype<T>>();
            }
            else {
                return std::type_identity<subtype<T>>{}; // here
            } 
        }
    }
    
    template<Iteratable T>
    using deepest_subtype = typename decltype(
                                detail::deepest_subtype_recursive<T>())::type;
    

    It's worth noting that you don't have to make your own wheels, the standard already provides something like Iteratable and subtypes, namely ranges::range and ranges::range_value_t.