constexpr 字符串文字检查:语法短,没有运行时可能性

constexpr string-literal checking: short syntax, no runtime possibility

编辑:重命名,因为我的最终解决方案不使用中毒方法。

我正在寻找一种方法来防止在运行时调用 constexpr 方法。我正在编写一个接受字符串文字的函数,所以我不能简单地使用 NTTP 作为需要 constexpr 参数的方法:

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

因为这样即使是合法的 constexpr 使用也会变得很麻烦,需要值具有静态 linkage,并且您不能输入字符串文字。我想做:

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

原因是因为我根据值列表检查字符串,并想静态检查参数是否包含在允许的集合中。我可以很容易地做到这一点:只需在 constexpr 函数中 throw()'ing,就可能导致编译时错误。但我不希望使用某些导致程序在运行时退出的分支生成生产代码。那会在我的领域引起一个大问题;此功能在执行“其他重要任务”的程序中是一个不错的选择,如果程序终止,就会发生不好的事情。

我阅读了一大堆可能的方法来做到这一点:

  1. 使用 C++20 consteval - 没有 C++20
  2. 使用 C++20 std::is_constant_evaluated - 没有 C++20
  3. 通过 return 将结果转换为未定义的符号(例如 extern int i 永远不会被定义),从而在运行时中毒该方法。如果在编译时调用该方法,编译器永远不会创建包含该符号的 return 代码,但如果在运行时调用该方法,它会创建代码,从而导致 link 错误。有效,但丑陋 linker 错误;不是我最喜欢的。我的 post 中显示了一个版本:.
  4. 在 C++17 中,noexcept 会自动添加到对实际上在 constexpr 上下文中调用的 constexpr 函数的任何调用中。所以你可以做 noexcept(foo(1)) vs noexcept(foo(i)) for constexpr int foo(int i) (未明确声明 noexcept) 来检测 i 是否导致调用 constexpr/不是。但是您不能在接受了某些参数的 constexpr 函数中执行此操作 - 您需要从调用站点执行此操作。所以:可能需要一个辅助 MACRO(不是我最喜欢的,但它有效)。
  5. 如果 lambda 范围外的某些变量不是 constexpr,则创建一个 return 类型无效的 lambda。这个post 进入细节:

所以我倾向于使用 #3 或 #4 + 一个宏,但是 *** 这个 post 大约是 #5 ***,或者全新的想法。

例如,谁能想出一种不用 lambda 来完成#5 的方法吗?在那之后,我想看看我是否可以想出一种在 constexpr 函数本身中使用它而不是要求从调用站点使用它的方法。现在,如果 constexpr 函数在运行时被调用,就试着毒化它,忘记“检测”函数调用是否是 constexpr.

我可以像作者那样通过在 main 中创建一个 lambda 来重新创建 #5 的结果,但这实际上并不是很有用,而且我仍然不相信它是完全合法的。首先,任何可以使用 lambda 完成的事情都可以在没有 lambda 的情况下完成——对吗???如果没有 lambda,我什至无法让原作者的方法工作。这似乎是让它在 main().

之外工作的第一步

以下是我尝试在没有 lambda 的情况下重新创建 #5 的几个想法。有 10 亿多个排列的实例,none 其中有效: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) 调用站点语法使用它们变得非常冗长,(2) 尽管调用站点使用 MyTags::TAG_ONE 这样的语法没问题,但我的程序需要能够知道完整的标签集,所以它们需要在数组或模板变量中,(3) 使用数组是行不通的,因为获取数组元素会产生一个 rvalue,它没有linkage,所以不能作为 NTTP 提供,(4) 使用具有显式特化的模板变量来定义每个标记要求模板变量是全局范围的,这对我来说效果不佳。 ..

这大概是我能做的最好的了 - 我觉得有点丑...:[=​​41=]

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")>();
}

这是我自己的答案,它检查字符串文字是否在 COMPILE-TIME 的允许集合内,然后根据该字符串的值执行操作。不需要 constexpr 函数中毒,并且仍然没有繁琐的要求来提供带有静态链接的字符串文字。

“shorthand 选项 2”归功于 Jarod42,它使用字符串模板 user-defined 文字的 gcc 扩展,它是 C++20 的一部分,但不是 C ++17.

我想我对这三种“shorthand”call-site 语法中的任何一种都很满意。我仍然欢迎任何替代方案或改进,或对我搞砸的事情提出建议。完美转发等留作reader的练习;-)

现场演示: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..., '[=10=]'};

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