Общий способ ленивого вычисления (короткого замыкания) условных типов шаблонов

Во время игры со строкой времени (переменные списки char) для компиляции мне нужно было реализовать способ проверки, содержит ли строка времени компиляции другую (меньшую) строку времени компиляции.

Это была моя первая попытка:

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...>;
    static constexpr int sz1{L1::size};
    static constexpr int sz2{L2::size};

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

Я считаю, что это решение чрезвычайно легко читать и рассуждать. К сожалению, он не работает.

Компилятор всегда пытается создать экземпляр каждой отдельной ветки std::conditional, даже тех, которые не приняты. Другими словами, короткого замыкания не происходит.

Это приводит к бесконечному созданию экземпляра Contains.

Я решил свою первоначальную проблему, разделив каждый блок std::conditional в отдельном классе шаблонов, где результаты условия обрабатываются как частичные специализации.

Это работает, но, к сожалению, мне очень трудно читать/изменять.


Есть ли способ ленизировать экземпляр типа шаблона и быть близким к моему оригинальному решению?

Это пример того, как выглядит код:

using Type = std::conditional
<
    (I1 >= sz1),
    std::false_type,
    std::conditional
    <
        (L1::template at<I1>() != L2::template at<I2>()),
        DeferInstantiation<typename Contains<I1 + 1, 0, L1, L2>::Type>,
        std::conditional
        <
            (I2 == sz2 - 1),
            std::true_type,
            DeferInstantiation<typename Contains<I1 + 1, I2 + 1, L1, L2>::Type>
        >
    >
>;

Возможно ли реализовать DeferInstantiation<T>?

Ответ 1

Вот общий шаблон, позволяющий отложить создание экземпляра, просто не создавая экземпляры:)

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...>;
};

Для полноты, простой пример, демонстрирующий его использование:

#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;
}

Отпечатки:

OneParam
TwoParam

Ответ 2

Компилятор всегда пытается создать экземпляр каждой ветки std:: условный, даже тех, которые не были приняты. Другими словами, короткого замыкания не происходит.

std::conditional<B,T,F> предоставляется с целью выполнения компиляции выбор между заданными типами T и F, в зависимости от булева B. выбор осуществляется по специализации. Когда значение B истинно, инстанцированная специализация:

std::conditional<true,T,F>
{
    typedef T type;
};

И когда B является ложным, специализированная специализация:

std::conditional<false,T,F>
{
    typedef F type;
};

Обратите внимание, что для создания экземпляра любой специализации оба T и F должны быть инстанцированным. Нет "ветвей". Понятие "короткое замыкание" создание либо std::conditional<true,T,F>, либо std::conditional<false,T,F> может означать только не делать этого.

Нет, невозможно реализовать DeferInstantiation<U>, для параметра типа U, так что инстанцирование

std::conditional<{true|false},DeferInstantiation<T>,DeferInstantiation<F>>

не влечет за собой создание экземпляров DeferInstantiation<T> и DeferInstantiation<F>>, и, следовательно, T и F.

Для выполнения выбора в соответствии с тем, какие или два или более шаблонов должны быть экземпляр, язык предоставляет специализацию (как показано на рисунке по определению самого std::conditional<B,T,F>); он обеспечивает перегрузку шаблона функции и SFINAE. Специализация и разрешение перегрузки могут быть синергетически связаны с SFINAE через библиотечную поддержку std::enable_if<B,T>

Проблема, которая мешала вам в создании конкретной рекурсивной мета-функции что вы хотите, это не выбор между заданными типами, а выбор шаблона в который должна быть направлена ​​рекурсивная инстанцировка. std::conditional не с целью. Ответ @Pradhan показывает, что шаблон отличается от std::conditional может быть написано для выполнения выбора между двумя шаблонами, без влекущие за собой то, что оба они должны быть созданы. Он применяет специализацию для этого.

Как вы говорите, вы уже определили решение специализации для проблема. Это в принципе правильный способ рекурсивного управления выбор шаблона в рекурсивных мета-функциях. Однако с появлением constexpr, рекурсивные мета-функции не дают ничего подобного рыночной доле проблемы, которые они ранее делали, и большая часть головной боли, которые они вызывали ушла в прошлое.

Частная проблема здесь - определение в compiletime, является ли одна строка подстрокой другого - может быть решена без сбоев с метапрограммой шаблона и без представляющие строки компиляции в противном случае, как традиционные строковые литералы:

#include <cstddef>

constexpr std::size_t str_len(char const *s)
{
    return *s ? 1 + str_len(s + 1) : 0;
}

constexpr bool 
is_substr(char const * src, char const *targ, 
            std::size_t si = 0, std::size_t ti = 0)
{
    return  !targ[ti] ? true :
                str_len(src + si) < str_len(targ + ti) ? false :
                    src[si] == targ[ti] ? 
                        is_substr(src,targ,si + 1, ti + 1) :
                            is_substr(src,targ,si + 1, 0);
}

// Compiletime tests...

static_assert(is_substr("",""),"");
static_assert(is_substr("qwerty",""),"");
static_assert(is_substr("qwerty","qwerty"),"");
static_assert(is_substr("qwerty","qwert"),"");
static_assert(is_substr("qwerty","werty"),"");
static_assert(is_substr("qwerty","wert"),"");
static_assert(is_substr("qwerty","er"),"");
static_assert(!is_substr("qwerty","qy"),"");
static_assert(!is_substr("qwerty","et"),"");
static_assert(!is_substr("qwerty","qwertyz"),"");
static_assert(!is_substr("qwerty","pqwerty"),"");
static_assert(!is_substr("","qwerty"),"");

int main()
{
    return 0;
}

Это будет компилироваться как С++ 11 или лучше.

У вас могут быть причины, по которым вы хотите представлять строки команд как CharList<char ...> кроме того, что делает их поддающимися TMP компиляционные запросы, такие как это. Мы можем видеть, что CharList<char ...Cs> имеет член статической константы size, оценивающий до sizeof...(Cs), и имеет статическая функция-член at<N>(), оценивающая значение N th ...Cs. В этом случае (при условии, что at<N>() отлаживается), вы можете адаптировать is_substr - функция шаблона, ожидающая CharList<char ...> параметры примерно на следующих строках:

#include <type_traits>

template<
    class SrcList, class TargList, std::size_t SrcI = 0, std::size_t TargI = 0>
constexpr typename 
std::enable_if<(TargI == TargList::size && SrcI <= SrcList::size),bool>::type 
is_substr()
{
    return true;
}

template<
    class SrcList, class TargList, std::size_t SrcI = 0, std::size_t TargI = 0>
constexpr typename 
std::enable_if<(TargI < TargList::size && SrcI == SrcList::size),bool>::type 
is_substr()
{
    return false;
}

template<
    class SrcList, class TargList, std::size_t SrcI = 0, std::size_t TargI = 0>
constexpr typename 
std::enable_if<(TargI < TargList::size && SrcI < SrcList::size),bool>::type 
is_substr()
{
    return  SrcList::template at<SrcI>() == TargList::template at<TargI>() ? 
                is_substr<SrcList,TargList,SrcI + 1,TargI + 1>() :
                is_substr<SrcList,TargList,SrcI + 1,0>();
}

который иллюстрирует применение SFINAE, используя std::enable_if

Наконец, вы также можете быть заинтересованы в этой программе:

#include <iostream>

template<char const * Arr>
struct string_lit_type 
{
    static constexpr const char * str = Arr;
    static constexpr std::size_t size = str_len(str);
    static constexpr char at(std::size_t i) {
        return str[i];
    }
};

constexpr char arr[] = "Hello World\n";

int main()
{
    std::cout << string_lit_type<arr>::str;
    std::cout << string_lit_type<arr>::size << std::endl;
    std::cout << string_lit_type<arr>::at(0) << std::endl;
    return 0;
}

который печатает:

Hello World
12
H

(Код, скомпилированный с g++ 4.9, clang 3.5)