constexpr 문자열 리터럴 검사 : 짧은 구문, 런타임 가능성 없음

Aug 21 2020

편집 : 내 최종 솔루션이 중독 방법을 사용하지 않기 때문에 이름이 변경되었습니다.

런타임에 constexpr 메서드가 호출되는 것을 방지하는 방법을 찾고 있습니다. 문자열 리터럴을 받아들이는 함수를 작성 중이므로 constexpr매개 변수 를 요구하는 방법으로 NTTP를 간단히 사용할 수는 없습니다 .

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

합법적 인 constexpr 사용조차도 번거롭기 때문에 값에 정적 연결이 필요하고 문자열 리터럴을 공급할 수 없습니다. 나하고 싶어:

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

그 이유는 값 목록에 대해 문자열을 확인하고 매개 변수가 허용 된 집합에 포함되어 있는지 정적으로 확인하기 때문입니다. 나는 이것을 쉽게 할 수있다. 단지 함수에 throw()'ing in a constexprfunction, you can cause a compile-time error. 그러나 프로그램이 런타임에 종료되도록하는 일부 분기로 프로덕션 코드를 생성 할 가능성을 원하지 않습니다. 그것은 내 분야에서 큰 문제를 일으킬 것입니다. 이 기능은 "기타 중요한 작업"을 수행하는 프로그램에서 유용하며 프로그램이 종료되면 나쁜 일이 발생합니다.

이 작업을 수행 할 수있는 여러 가지 가능한 방법에 대해 읽었습니다.

  1. C ++ 20 사용-C ++ 20 consteval없음
  2. C ++ 20 사용-C ++ 20 std::is_constant_evaluated없음
  3. 정의되지 않은 기호 (예 : 정의되지 않은 기호)로 결과를 반환하여 런타임에 메서드를 독살하십시오 extern int i. 컴파일러는 메서드가 컴파일 타임에 호출되는 경우 해당 기호를 반환하는 코드를 생성하지 않지만 런타임에 메서드가 호출되면 링크 오류가 발생합니다. 작동하지만 못생긴 링커 오류; 내가 제일 좋아하는 게 아니야. 그 버전은 여기 내 게시물에 나와 있습니다 : Constexpr functions not called at compile-time if result is ignore .
  4. C ++ 17에서는 컨텍스트 에서 실제로 noexcept호출되는 constexpr함수 에 대한 호출에 자동으로 추가 constexpr됩니다. 따라서 호출이 / not이 되는지 여부를 감지하기 위해 noexcept(foo(1))vs noexcept(foo(i))for constexpr int foo(int i)(명시 적으로 선언되지 않음 noexcept)를 수행 할 i수 있습니다 constexpr. 그러나 constexpr일부 매개 변수를 수락 한 함수 내에서는이를 수행 할 수 없습니다 . 호출 사이트에서 수행해야합니다. 그래서 : 아마도 도우미 매크로가 필요합니다 (내가 좋아하는 것은 아니지만 작동합니다).
  5. 람다 범위 밖의 특정 변수가 아닌 경우 반환 유형이 잘못된 람다를 만듭니다 constexpr. 이 게시물은 자세히 설명합니다.https://stackoverflow.com/a/40413051

그래서 저는 # 3 또는 # 4 + 매크로를 사용하는쪽으로 기울고 있지만 ***이 게시물은 # 5에 관한 것입니다 *** 또는 완전히 새로운 아이디어입니다.

예를 들어 누구나 람다없이 # 5를 할 수있는 방법을 생각 해낼 수 있습니까? 그 후 constexpr호출 사이트에서 사용하도록 요구하지 않고 함수 자체 내에서 사용하는 방법을 찾을 수 있는지 확인하고 싶습니다 . 지금 constexpr은 런타임에 호출 되는 함수 를 독살하려고 시도 하고 함수 호출이인지 여부를 "감지"하는 것은 잊어 버리십시오 constexpr.

main저자가했던 것처럼 람다를 생성하여 # 5의 결과를 재현 할 수 있지만 실제로는 그다지 유용하지 않으며 여전히 완전히 합법적이라고 확신하지 못합니다. 우선 람다로 할 수있는 것은 람다없이 할 수 있습니다. 맞죠 ??? 람다 없이는 원래 저자의 방법을 사용할 수도 없습니다. 외부에서 작동하도록하는 데 필요한 첫 번째 단계 인 것 같습니다 main().

아래는 람다없이 # 5를 재현하려고 시도한 몇 가지 아이디어입니다. 10 억 개 이상의 순열이있는 실제 예는 작동하지 않습니다.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오면 연결이없는을 생성 하므로 a로 공급할 수 없습니다. 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")>();
}

답변

Sean Aug 24 2020 at 20:32

다음은 문자열 리터럴이 COMPILE-TIME에 허용 된 집합 내에 있는지 확인한 다음 해당 문자열의 값에 따라 작업을 수행하는 내 답변입니다. constexpr함수의 중독 이 필요하지 않으며 정적 연결이있는 문자열 리터럴을 제공하기위한 번거로운 요구 사항도 여전히 없습니다.

gccC ++ 20의 일부이지만 C ++ 17이 아닌 문자열 템플릿 사용자 정의 리터럴에 대한 확장을 사용하는 "축약 형 옵션 2"에 대한 크레딧은 Jarod42로 이동합니다 .

세 가지 "속기"호출 사이트 구문 중 하나에 만족한다고 생각합니다. 나는 여전히 어떤 대안이나 개선점 또는 내가 엉망으로 만든 것에 대한 조언을 환영합니다. 퍼펙트 포워딩 등은 독자를위한 연습 문제로 남겨집니다 ;-)

라이브 데모 : 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;
}