Search code examples
c++templatesc++14template-meta-programmingtype-traits

Generic way of lazily evaluating (short-circuiting) with std::conditional_t


While playing around with compile-time string (variadic lists of char) manipulation, I needed to implement a way of checking if a compile-time string contained another (smaller) compile-time string. This was my first attempt:

template<int I1, int I2, typename, typename> struct Contains;

template<int I1, int I2, char... Cs1, char... Cs2> 
struct Contains<I1, I2, CharList<Cs1...>, CharList<Cs2...>>
{
    using L1 = CharList<Cs1...>;
    using L2 = CharList<Cs2...>;

    using Type = std::conditional_t
    <
        (I1 >= sizeof...(Cs1)),
        std::false_type,
        std::conditional
        <
            (L1::template at<I1>() != L2::template at<I2>()),
            typename Contains<I1 + 1, 0, L1, L2>::Type,
            std::conditional
            <
                (I2 == sizeof...(Cs2) - 1),
                std::true_type,
                typename Contains<I1 + 1, I2 + 1, L1, L2>::Type
            >
        >
    >;
};

I find this solution extremely easy to read and reason about. Unfortunately, it doesn't work.

The compiler always tries to instantiate every single branch of std::conditional, even those which are not taken. To put it in another way, short-circuiting isn't happening. This causes Contains to be instantiated infinitely.

I've solved my original problem by separating every std::conditional block in a separate template class where the condition results are handled as partial specializations. It works, but unfortunately I find it very hard to read/modify.


Is there a way to lazily instantiate a template type and be close to my original solution?

I was thinking that I could wrap the greedily instantiated templates in a type like:

DeferInstantiation<typename Contains<I1 + 1, 0, L1, L2>::Type>

This should prevent greedy instantiations of the template argument. Is it somehow possible to implement DeferInstantiation<T>?


Solution

  • Here's a generic template to allow deferred instantiation by simply not instantiating :)

    template <bool B, template <typename...> class TrueTemplate, template <typename...> class FalseTemplate, typename ArgsTuple>
    struct LazyConditional;
    
    template <template <typename...> class TrueTemplate, template <typename...> class FalseTemplate, typename ... Args>
    struct LazyConditional<true, TrueTemplate, FalseTemplate, std::tuple<Args...>>
    {
      using type = TrueTemplate<Args...>;
    };
    
    template <template <typename...> class TrueTemplate, template <typename...> class FalseTemplate, typename ... Args>
    struct LazyConditional<false, TrueTemplate, FalseTemplate, std::tuple<Args...>>
    {
      using type = FalseTemplate<Args...>;
    };
    

    For completeness, a simple example demonstrating its use:

    #include <iostream>
    #include <type_traits>
    #include <tuple>
    
    template <typename T>
    struct OneParam
    {
      void foo(){std::cout << "OneParam" << std::endl;}
    };
    
    template <typename T, typename U>
    struct TwoParam
    {
      void foo(){std::cout << "TwoParam" << std::endl;}
    };
    
    template <bool B, template <typename...> class TrueTemplate, template <typename...> class FalseTemplate, typename ArgsTuple>
    struct LazyConditional;
    
    template <template <typename...> class TrueTemplate, template <typename...> class FalseTemplate, typename ... Args>
    struct LazyConditional<true, TrueTemplate, FalseTemplate, std::tuple<Args...>>
    {
      using type = TrueTemplate<Args...>;
    };
    
    template <template <typename...> class TrueTemplate, template <typename...> class FalseTemplate, typename ... Args>
    struct LazyConditional<false, TrueTemplate, FalseTemplate, std::tuple<Args...>>
    {
      using type = FalseTemplate<Args...>;
    };
    
    template <typename ... Args>
    struct OneOrTwoParam
    {
      using type = typename LazyConditional<sizeof...(Args)==1, OneParam, TwoParam, std::tuple<Args...> >::type;
    };
    
    int main()
    {
      OneOrTwoParam<int>::type().foo();
      OneOrTwoParam<int, int>::type().foo();
      return 0;
    }
    

    This prints:

    OneParam
    TwoParam