Search code examples
c++variadic-templatesctad

CTAD with variable number of template arguments


For unit testing, I wrote a class ScopeSet which changes the values of variables in the current scope, and at the end of the scope the original values are set back. Usage example:

int i = 1;
double d = 2.;
{
    auto _ = ScopeSet( i, 10, d, 20. );
    assert( i == 10 );
    assert( d == 20. );
}
assert( i == 1 );
assert( d == 2. );

I have finished the class, except for the deduction guides. ScopeSet takes a variable number of variable-value pairs. So far I have written the deduction guides by hand for up to three pairs

template< typename T0, typename U0 > ScopeSet( T0 &, U0 ) -> ScopeSet< T0 >;
template< typename T0, typename U0, typename T1, typename U1 > ScopeSet( T0 &, U0, T1 &, U1 ) -> ScopeSet< T0, T1 >;
template< typename T0, typename U0, typename T1, typename U1, typename T2, typename U2 > ScopeSet( T0 &, U0, T1 &, U1, T2 &, U2 ) -> ScopeSet< T0, T1, T2 >;

My question is, how can I write a deduction guide which works for a variable number of pairs?


Solution

  • You could start with a class to store a reference to a single variable, that assigns a new value to the variable at construction and restores the old value on destruction:

    template <class T>
    struct ScopePair {
        template <class V>
        ScopePair(T& var, V&& val) : variable(var), saved(std::move(var)) {
            variable = std::forward<V>(val);
        }
        ~ScopePair() { variable = std::move(saved); }
    
        T& variable;
        T saved;
    };
    template <class T, class U>
    ScopePair(T, U) -> ScopePair<std::remove_cvref_t<T>>;
    

    This ScopePair is usable as-is if you only need to handle a single variable.

    The ScopeSet can then consist of any number of such ScopePairs by recursively inheriting from a ScopeSet where one pair of arguments are picked off at a time:

    template <class... Ts> struct ScopeSet {};
    
    template <class T, class V, class... Rs>
    struct ScopeSet<T, V, Rs...> : ScopeSet<Rs...> {
        template <class VV, class... RRs>
        ScopeSet(T& t, VV&& v, RRs&&... rest)
            : ScopeSet<Rs...>(std::forward<RRs>(rest)...),
              pair(t, std::forward<VV>(v)) {}
    
        ScopePair<T> pair;
    };
    template <class... Ts>
    ScopeSet(Ts...) -> ScopeSet<std::remove_cvref_t<Ts>...>;
    

    Demo

    The above makes ScopeSet( i, 10, d, 20. ); into a ScopeSet<int, int, double, double> which is much easier than to shave off every other type.


    If you absolutely must have a one int and one double you could add a specialization for ScopeSet<ScopeSetTypes<Ts...>> that inherits from ScopeSet<Ts...>. Using the deduction guide will then make ScopeSet( i, 10, d, 20. ); into a ScopeSet<ScopeSetTypes<int, double>>:

    template<class...Ts> struct ScopeSetTypes{};
    
    template <class... Ts>
    struct ScopeSet<ScopeSetTypes<Ts...>> : ScopeSet<Ts...> {
        using ScopeSet<Ts...>::ScopeSet;
    };
    
    // The deduction guide:
    template <class... Ts>
    ScopeSet(Ts...) -> ScopeSet<
        decltype([]<std::size_t... Is>(std::index_sequence<Is...>)
                     -> ScopeSetTypes<std::tuple_element_t<
                         Is * 2, std::tuple<std::remove_cvref_t<Ts>...>>...> {
        }(std::make_index_sequence<sizeof...(Ts) / 2>()))>;
    

    Demo