Search code examples
c++c++17variadic-templates

Using a pack expansion with an index - is it UB?


The code below "seems" to work - however, I'm a little concerned that I'm in the realms of unspecified behaviour at the marked point. If I am, can someone please throw me a bone so that I can ensure that I'm not going to have it suddenly break when I change compiler?

The intent (in case it isn't clear) is that I want to generate a std::function that is able to wrap another - but process the arguments in a slightly different way.

/// Some collection of arguments generated at runtime.
class ArgCollection 
{
   int argCount;
   std::variant *arguments;
}

/// generate the wrapping fn
template<class ...Args>
std::function<void(ArgCollection)> GetConvert(std::function<void(Args...)> thing)
{
    constexpr std::size_t argCount = sizeof...(Args);
    return [argCount, method](const ArgCollection& args) -> void {
        if (args.numArguments != argCount)
            throw std::invalid_argument("Invalid number of arguments");

        int count = 0;  <------------ I fear about the usage of this variable.
        auto convertedArgs = std::make_tuple(ConvertArg<Args>(args, count++)...);
        std::apply(method, convertedArgs);
    };
}

/// helper const & reference stripping
template<typename T>
using base_type = typename std::remove_cv<typename std::remove_reference<T>::type>::type;

/// Get the idx'th argument, and convert it to what we can hand to the function
template<class T>
static base_type<T> ConvertArg(const ArgCollection &args, int idx)
{
    return base_type<T>(args[idx]);
}

Solution

  • auto convertedArgs = std::make_tuple(ConvertArg<Args>(args, count++)...);
    

    the increments are indeterminately sequenced relative to each other. Compilers are free to do them in any order, and change the order because a butterfly flaps its wings. (Prior to the guarantees where worse than this)

    In there is an easy work around:

    constexpr std::size_t argCount = sizeof...(Args);
    return [&]<std::size_t...Is>(std::index_sequence<Is...>){
      return [argCount, method](const ArgCollection& args) -> void {
        if (args.numArguments != argCount)
            throw std::invalid_argument("Invalid number of arguments");
    
        auto convertedArgs = std::make_tuple(ConvertArg<Args>(args, Is)...);
        std::apply(method, convertedArgs);
      };
    }( std::make_index_sequence<sizeof...(Args)>{} );
    

    where we make an index sequence object and unpack it in a lambda within the function.

    In you basically need helper functions that build and unpack the indexes.

    template<auto x>
    using constant_t = std::integral_constant<std::decay_t<decltype(x)>, x>;
    template<auto x>
    constexpr constant_t<x> constant_v={};
    
    template<std::size_t...Is, class F>
    decltype(auto) index_over( std::index_sequence<Is...>, F&& f ) {
      return f( constant_v<Is>... );
    }
    template<std::size_t N, class F>
    decltype(auto) index_upto(F&& f) {
      return index_over( std::make_index_sequence<N>{}, std::forward<F>(f) );
    }
    

    then your code becomes:

    constexpr std::size_t argCount = sizeof...(Args);
    return index_upto<argCount>([&](auto...Is){
      return [argCount, method, Is...](const ArgCollection& args) -> void {
        if (args.numArguments != argCount)
            throw std::invalid_argument("Invalid number of arguments");
    
        auto convertedArgs = std::make_tuple(ConvertArg<Args>(args, Is)...);
        std::apply(method, convertedArgs);
      };
    });
    

    or somesuch.

    You can also write a more conventional helper function that you pass an index sequence to.

    Finally, you can rely on the fact that {} based initialization is ordered.

    template<class ...Args>
    std::function<void(ArgCollection)> GetConvert(std::function<void(Args...)> thing)
    {
        constexpr std::size_t argCount = sizeof...(Args);
        return [argCount, method](const ArgCollection& args) -> void {
            if (args.numArguments != argCount)
                throw std::invalid_argument("Invalid number of arguments");
    
            int count = 0;  <------------ I fear about the usage of this variable.
            auto convertedArgs = std::tuple{ConvertArg<Args>(args, count++)...};
            std::apply(method, convertedArgs);
        };
    }
    

    which could be easier.