constexpr проверка строкового литерала: короткий синтаксис, нет возможности выполнения

РЕДАКТИРОВАТЬ: переименовано, поскольку мое окончательное решение не использует метод отравления.

Я ищу способ предотвратить вызов метода constexpr во время выполнения. Я пишу функцию, которая принимает строковый литерал, поэтому я не могу просто использовать NTTP как способ требоватьconstexpr параметр:

template<const char* str>
auto func() {...}

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

constexpr auto func(const char* str) {...}

Причина в том, что я проверяю строку по списку значений и хочу СТАТИЧНО проверить, содержится ли параметр в разрешенном наборе. Я могу сделать это легко: простоthrow()в constexprфункция, вы можете вызвать ошибку времени компиляции. Но мне не нужна возможность создания производственного кода с какой-либо веткой, которая заставляет программу завершаться во время выполнения. Это вызвало бы серьезную проблему в моей области; эта функция полезна в программе, которая выполняет "другие важные вещи", и при завершении программы случаются неприятности.

Я читал о множестве возможных способов сделать это:

  1. Используйте C++20 consteval - нет C++20
  2. Используйте C++20 std::is_constant_evaluated - нет C++20
  3. Отравить метод во время выполнения, вернув результат неопределенному символу (например, extern int iэто никогда не определяется). Компилятор никогда не создает код, который возвращает этот символ, если метод вызывается во время компиляции, но он делает это, если метод вызывается во время выполнения, что приводит к ошибке ссылки. Работает, но некрасивая ошибка компоновщика; не мой любимый. Версия этого показана в моем сообщении здесь: Функции Constexpr не вызываются во время компиляции, если результат игнорируется.
  4. В C++17 noexcept автоматически добавляется к любому вызову в constexpr функция, которая фактически вызывается в constexprконтекст. Так ты можешь сделатьnoexcept(foo(1)) против noexcept(foo(i)) за constexpr int foo(int i) (явно не указано noexcept), чтобы определить, i заставляет призыв быть constexpr/не. Но вы не можете сделать это изнутриconstexprфункция, принявшая какой-то параметр - это нужно делать с call-сайта. Итак: вероятно, требуется вспомогательный МАКРОС (не мой любимый, но он работает).
  5. Создайте лямбда, возвращаемый тип которой недопустим, если некоторые переменные вне области действия лямбды не являются constexpr. В этом сообщении подробно рассказывается: /questions/12854584/obnaruzhenie-vo-vremya-kompilyatsii-ili-vo-vremya-vyipolneniya-v-funktsii-constexpr/12854603#12854603

Так что я склоняюсь к использованию №3 или №4 + макрос, но *** этот пост о №5 *** или совершенно новых идеях.

Может ли кто-нибудь придумать, например, способ №5 без лямбды? После этого я хочу посмотреть, смогу ли я найти способ использовать его вconstexprфункция сама по себе, а не требует, чтобы она использовалась с сайта вызова. А пока просто попробуйте отравитьconstexpr функция, если она вызывается во время выполнения, забудьте об "обнаружении" того, является ли вызов функции constexpr.

Я могу воссоздать результаты #5, создав лямбду в mainкак и автор, но на самом деле это не очень полезно, и я все еще не уверен, что это полностью законно. Начнем с того, что все, что можно сделать с помощью лямбды, можно сделать и без лямбды, верно??? Я даже не могу заставить работать оригинальный авторский метод без лямбды. Это похоже на первый необходимый шаг, чтобы заставить его работать за пределамиmain().

Ниже приведены несколько идей, которые я пытался воссоздать № 5 без лямбда. Живой пример с еще миллиардом перестановок, ни одна из которых не работает: https://onlinegdb.com/B1oRjpTGP

// Common
template<int>
using Void = void;

// Common
struct Delayer {
    constexpr auto delayStatic(int input) { return input; }
};

// Attempt 1
template<typename PoisonDelayer>
constexpr auto procurePoison(int i) {
    struct Poison {
        // error: use of parameter from containing function
        // constexpr auto operator()() const -> Void<(PoisonDelayer::delayStatic(i), 0)> {}
    } poison;
    
    return poison;
}

// Attempt 2
struct PoisonInnerTemplate {
    const int& _i;

    // Internal compiler error / use of this in a constexpr
    template<typename PoisonDelayer>
    auto drink() const -> Void<(PoisonDelayer::delayStatic(_i), 0)> {}
};

int main()
{
    auto attempt1 = procurePoison<Delayer>(1);
    
    constexpr int i = 1;
    auto attempt2 = PoisonInnerTemplate{i};
    attempt2.drink<Delayer>();

    return 0;
}

Еще одна вещь: я поигрался с идеей создать заранее определенный список разрешенных тегов (обернуть строку в структуру, чтобы она могла быть NTTP) и поместить их в какой-то контейнер, а затем иметь метод для получения их. Проблемы следующие: (1) синтаксис call-site становится довольно подробным для их использования, (2) хотя было бы нормально для call-site использовать синтаксис вродеMyTags::TAG_ONE, моя программа должна знать полный набор тегов, поэтому они должны находиться в массиве или переменной шаблона, (3) использование массива не работает, потому что получение элемента массива создает rvalue, который не имеет связи, поэтому не может подаваться как NTTP, (4) с использованием переменной шаблона с явной специализацией для определения каждого тега требуется, чтобы переменная шаблона имела глобальную область видимости, что не подходит для меня...

Это лучшее, что я могу сделать - я думаю, это некрасиво...:

struct Tag {
    const char* name;
};

template<auto& tag>
void foo() {}

struct Tags {
    static constexpr Tag invalid = {};
    static constexpr Tag tags[] = {{"abc"}, {"def"}};

    template<size_t N>
    static constexpr Tag tag = tags[N];
    
    template<size_t N = 0>
    static constexpr auto& getTag(const char* name) {
        if constexpr(N<2) {
            if(string_view(name)==tag<N>.name) {
                return tag<N>;
            } else {
                return getTag<N+1>(name);
            }
        } else {
            return invalid;
        }
    }
};

int main()
{
    foo<Tags::getTag("abc")>();
}

1 ответ

Решение

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

Благодарим Jarod42 за "сокращенный вариант 2", в котором используется gcc расширение для определяемых пользователем литералов строкового шаблона, которое является частью C++20, но не C++17.

Я думаю, что меня вполне устраивает любой из трех "сокращенных" синтаксисов call-site. Я все равно приветствовал бы любые альтернативы или улучшения, или указатели на то, что я напутал. Идеальная пересылка и т. Д. Оставлена ​​в качестве упражнения для читателя;-)

Живая демонстрация: https://onlinegdb.com/S1K_7sb7D

// Helper for Shorthand Option 1 (below)
template<typename Singleton>
Singleton* singleton;

// Helper to store string literals at compile-time
template<typename ParentDispatcher>
struct Tag {
    using Parent = ParentDispatcher;
    const char* name;
};

// ---------------------------------
// DISPATCHER:
// ---------------------------------
// Call different functions at compile-time based upon
// a compile-time string literal.
// ---------------------------------

template<auto& nameArray, typename FuncTuple>
struct Dispatcher {
    FuncTuple _funcs;
    
    using DispatcherTag = Tag<Dispatcher>;
    
    template<size_t nameIndex>
    static constexpr DispatcherTag TAG = {nameArray[nameIndex]};
    
    static constexpr DispatcherTag INVALID_TAG = {};

    Dispatcher(const FuncTuple& funcs) : _funcs(funcs) {
        singleton<Dispatcher> = this;
    }

    template<size_t nameIndex = 0>
    static constexpr auto& tag(string_view name) {
        if(name == nameArray[nameIndex]) {
            return TAG<nameIndex>;
        } else {
            if constexpr (nameIndex+1 < nameArray.size()) {
                return tag<nameIndex+1>(name);
            } else {
                return INVALID_TAG;
            }
        }
    }

    static constexpr size_t index(string_view name) {
        for(size_t nameIndex = 0; nameIndex < nameArray.size(); ++nameIndex) {
            if(name == nameArray[nameIndex]) {
                return nameIndex;
            }
        }
        return nameArray.size();
    }
    
    constexpr auto& operator()(const char* name) const {
        return tag(name);
    }

    template<auto& tag, typename... Args>
    auto call(Args... args) const {
        static constexpr size_t INDEX = index(tag.name);
        static constexpr bool VALID = INDEX != nameArray.size();
        static_assert(VALID, "Invalid tag.");

        return get<INDEX*VALID>(_funcs)(args...);
    }
};

template<auto& nameArray, typename FuncTuple>
auto makeDispatcher(const FuncTuple& funcs) {
    return Dispatcher<nameArray, FuncTuple>(funcs);
}

// ---------------------------------
// SHORTHAND: OPTION 1
// ---------------------------------
// Use a singleton pattern and a helper to let a tag be associated with a
// specific dispatcher, so that the call-site need not specify dispatcher twice
// ---------------------------------

template<auto& tag, typename... Args>
auto call(Args... args) {
    using Tag = remove_reference_t<decltype(tag)>;
    using ParentDispatcher = typename Tag::Parent;
    static auto dispatcher = singleton<ParentDispatcher>;

    return dispatcher->template call<tag>(args...);
}

// ---------------------------------
// SHORTHAND: OPTION 2
// ---------------------------------
// Use a string template user-defined literal operator to shorten call-site syntax
// gcc supports this as an extension implementing proposal N3599 (standardized in C++20)
// If warnings occur, try pragma GCC diagnostic ignored "-Wgnu-string-literal-operator-template"
// ---------------------------------

// Need characters to be in contiguous memory on the stack (not NTTPs) for TAG_FROM_LITERAL
template<char... name>
constexpr char NAME_FROM_LITERAL[] = {name..., '\0'};

// Don't need to specify Dispatcher with user-defined literal method; will use dispatcher.check<>()
struct TagFromLiteral {};

// Need to have a constexpr variable with linkage to use with dispatcher.check<>()
template<char... name>
constexpr Tag<TagFromLiteral> TAG_FROM_LITERAL = {NAME_FROM_LITERAL<name...>};

// Create a constexpr variable with linkage for use with dispatcher.check<>(), via "MyTag"_TAG
template<typename Char, Char... name>
constexpr auto& operator"" _TAG() {
    return TAG_FROM_LITERAL<name...>;
}

// ---------------------------------
// SHORTHAND: OPTION 3
// ---------------------------------
// Use a macro so the call-site need not specify dispatcher twice
// ---------------------------------

#define DISPATCH(dispatcher, name) dispatcher.call<dispatcher(name)>

// ---------------------------------
// COMMON: TEST FUNCTIONS
// ---------------------------------

bool testFunc1(int) { cout << "testFunc1" << endl; }
bool testFunc2(float) { cout << "testFunc2" << endl; }
bool testFunc3(double) { cout << "testFunc3" << endl; }

static constexpr auto funcs = make_tuple(&testFunc1, &testFunc2, &testFunc3);
static constexpr auto names = array{"one", "two", "three"};

int main()
{
    // Create a test dispatcher
    auto dispatcher = makeDispatcher<names>(funcs);

    // LONG-HAND: call syntax: a bit verbose, but operator() helps
    dispatcher.call<dispatcher("one")>(1);
    
    // SHORTHAND OPTION 1: non-member helper, singleton maps back to dispatcher
    call<dispatcher("one")>(1);

    // SHORTHAND OPTION 2: gcc extension for string UDL templates (C++20 standardizes this)
    dispatcher.call<"one"_TAG>(1);

    // SHORHAND OPTION 3: Macro
    DISPATCH(dispatcher, "one")(1);

    return 0;
}
Другие вопросы по тегам