3
\$\begingroup\$

Problem

Perfect forwarding is known to be imperfect when it comes to list-initialization; I'll use std::construct_at as an example:

std::vector a(3, 9);          // OK: [9, 9, 9]
std::construct_at(p, 3, 9);   // OK: [9, 9, 9]

std::vector b{3, 9};          // OK: [3, 9]
std::construct_at(p, {3, 9}); // error: can't deduce type from braced-init-list

Even for non-list-initialization, we have this problem:

using Data = std::tuple<std::vector<int>, std::vector<int>>;

Data c{(3, 9), (3, 9)}; // oops: [[0; 9], [0; 9]]
Data d{{3, 9}, {3, 9}}; // OK: [[3, 9], [3, 9]]

where [0; 9] means [0, 0, 0, 0, 0, 0, 0, 0, 0]. This problem can be worked around by explicitly creating the object, for movable types:

std::construct_at(p, std::vector{3, 9});      // OK: [9, 9, 9]
Data e{std::vector(3, 9), std::vector(3, 9)}; // OK: [[9, 9, 9], [9, 9, 9]]

For non-movable types, this workaround does not work, because guaranteed copy elision does not apply to rvalue references. Moreover, moving is unacceptable even for some movable types (e.g., std::array<int, 100000>). (Not to mention the verbosity.)

Existing solutions

Different facilities of standard library addresses this problem in a number of ways:

  • std::array — uses aggregate initialization instead of perfect forwarding;

  • std::optional & std::variant — have an overload for argument lists starting with an std::initializer_list.

However, these solutions all have drawbacks:

  • Aggregates can't have constructors;

  • Having an overload for std::initializer_list doesn't scale (how about arguments preceding initializer list arguments), and aggregate initialization still doesn't work (until C++20).

Other things (std::tuple, std::construct_at, etc.) completely ignore this problem, rendering themselves unusable with non-movable types ...

My solution

... well, actually, there's a way to work around the limitation of C++ not being able to forward prvalues — we can make wrappers that convert to the destination type, which enables us to do this:

std::construct_at(p, brace(3, 9)); // OK
Data e{paren(3, 9), paren(3, 9)};  // OK

Apparently, this solution is not as elegant as a hypothetical std::construct_at(p, {3, 9}), but it has a valuable property — it works seamlessly with ordinary perfect forwarding and non-intrusively adapt to existing things! This solution also only works since C++17, because guaranteed copy elision is required.

All the magic is powered by two surprisingly simple function templates brace and paren:

init_utils.hpp

#ifndef INIT_UTILS_HPP_t0EeVIMqJL
#define INIT_UTILS_HPP_t0EeVIMqJL

#include <tuple>
#include <type_traits>

namespace init_utils::detail {
    template <typename T, typename... Args>
    class list_traits {
        template <typename U = T>
        static constexpr auto check(int) noexcept(noexcept(U{std::declval<Args>()...}))
            -> decltype((void)U{std::declval<Args>()...}, std::true_type{});
        static constexpr auto check(...) noexcept(false)
            -> std::false_type;
    public:
        static constexpr bool constructible = decltype(check(0))::value;
        static constexpr bool nothrow = noexcept(check(0));
    };
}

namespace init_utils {
    template <typename T, typename... Args>
    struct is_list_constructible
        : std::bool_constant<detail::list_traits<T, Args...>::constructible> {};
    template <typename T, typename... Args>
    inline constexpr bool is_list_constructible_v =
        is_list_constructible<T, Args...>::value;

    template <typename T, typename... Args>
    struct is_nothrow_list_constructible
        : std::bool_constant<detail::list_traits<T, Args...>::nothrow> {};
    template <typename T, typename... Args>
    inline constexpr bool is_nothrow_list_constructible_v =
        is_nothrow_list_constructible<T, Args...>::value;

    template <typename T, typename Tuple>
    constexpr T list_make_from_tuple(Tuple&& tuple)
    {
        return std::apply(
            [](auto&&... args) -> T {
                return T{std::forward<decltype(args)>(args)...};
            },
            std::forward<Tuple>(tuple)
        );
    }

    template <typename... Args>
    class direct_init_args : std::tuple<Args&&...> {
        using Base = std::tuple<Args&&...>;
    public:
        using Base::Base;

        template <typename T,
                  std::enable_if_t<std::is_constructible_v<T, Args...>, int> = 0>
        constexpr operator T() noexcept(std::is_nothrow_constructible_v<T, Args...>)
        {
            return std::make_from_tuple<T>(std::move(static_cast<Base&>(*this)));
        }
    };

    template <typename... Args>
    class list_init_args : std::tuple<Args&&...> {
        using Base = std::tuple<Args&&...>;
    public:
        using Base::Base;

        template <typename T,
                  std::enable_if_t<is_list_constructible_v<T, Args...>, int> = 0>
        constexpr operator T() noexcept(is_nothrow_list_constructible_v<T, Args...>)
        {
            return list_make_from_tuple<T>(std::move(static_cast<Base&>(*this)));
        }
    };
}

namespace init_utils::wrappers {
    template <typename... Args>
    constexpr auto paren(Args&&... args) noexcept
    {
        return direct_init_args<Args...>(std::forward<Args>(args)...);
    }
    template <typename... Args>
    constexpr auto brace(Args&&... args) noexcept
    {
        return list_init_args<Args...>(std::forward<Args>(args)...);
    }
}

#endif

Usage example:

#include "init_utils.hpp"
#include <initializer_list>
#include <iostream>
#include <tuple>

// non-copyable, non-movable
struct S {
    int value;
    S(int, int)
        : value{1}
    {
    }
    S(std::initializer_list<int>)
        : value{2}
    {
    }
    S(S&&) = delete;
    S& operator=(S&&) = delete;
};

int main()
{
    using namespace init_utils::wrappers;
    std::tuple<S, S> tuple{paren(1, 2), brace(3, 4)};
    std::cout << std::get<0>(tuple).value << ' '
              << std::get<1>(tuple).value << '\n';
}

What do you think of this idea?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.