다시 방문한 C ++ 컴파일 타임 카운터


28

TL; DR

이 전체 게시물을 읽으려고 시도하기 전에 다음 사항을 숙지하십시오.

  1. 제시된 문제에 대한 해결책 은 나 자신의해 발견 되었지만 여전히 분석이 올바른지 알고 싶어합니다.
  2. fameta::counter나머지 몇 가지 단점을 해결하는 클래스 로 솔루션을 패키지했습니다 . github에서 찾을 수 있습니다 .
  3. 당신은 godbolt 에 대한 직장에서 그것을 볼 수 있습니다 .

모든 것이 시작된 방법

Filip Roséen은 2015 년에 타임 카운터컴파일 하는 흑 마법이 C ++에 있다는 것을 발견 / 발명 한 이후,이 장치에 약간 집착했습니다. 따라서 CWG 가 기능을 수행하기로 결정 했을 때 실망했지만 여전히 그들의 마음을 희망했습니다. 몇 가지 매력적인 사용 사례를 보여줌으로써 변경 될 수 있습니다.

그런 다음, 그래서 내가 다시 일 좀보고 결정 몇 년 전에 uberswitch의 ES가 중첩 될 수 있습니다 - 흥미로운 사용 사례, 내 의견으로는 - 만 발견 할 수 있다는 것이 더 이상 작동하지 않을 의 새로운 버전 문제 2118 이 열려있는 상태 (및 여전히 ) 인 경우에도 사용 가능한 컴파일러 : 코드는 컴파일되지만 카운터는 증가하지 않습니다.

이 문제는 Roséen 웹 사이트 와 최근에 stackoverflow에서도 보고되었습니다 . C ++는 컴파일 타임 카운터를 지원합니까?

며칠 전에 문제를 다시 해결하기로 결정했습니다.

나는 여전히 유효한 C ++ 인 더 이상 작동하지 않는 컴파일러에서 변경된 사항을 이해하고 싶었습니다. 이를 위해, 나는 누군가가 그것에 대해 이야기하기 위해 웹을 넓고 멀리 탐색했지만 아무 소용이 없습니다. 그래서 나는 실험을 시작했고 몇 가지 결론에 도달했습니다. 나는 여기에서 내 자신보다 지식이 많은 사람들로부터 피드백을 받기를 기대하고 있습니다.

아래에서는 명확성을 위해 Roséen의 원래 코드를 제시합니다. 작동 방식에 대한 설명은 그의 웹 사이트참조하십시오 .

template<int N>
struct flag {
  friend constexpr int adl_flag (flag<N>);
};

template<int N>
struct writer {
  friend constexpr int adl_flag (flag<N>) {
    return N;
  }

  static constexpr int value = N;
};

template<int N, int = adl_flag (flag<N> {})>
int constexpr reader (int, flag<N>) {
  return N;
}

template<int N>
int constexpr reader (float, flag<N>, int R = reader (0, flag<N-1> {})) {
  return R;
}

int constexpr reader (float, flag<0>) {
  return 0;
}

template<int N = 1>
int constexpr next (int R = writer<reader (0, flag<32> {}) + N>::value) {
  return R;
}

int main () {
  constexpr int a = next ();
  constexpr int b = next ();
  constexpr int c = next ();

  static_assert (a == 1 && b == a+1 && c == b+1, "try again");
}

g ++ 및 clang ++ 최근 컴파일러는 next()항상 1을 반환합니다. 조금 실험 해 보았을 때 적어도 g ++의 문제는 컴파일러가 함수를 처음 호출 할 때 함수 템플릿 기본 매개 변수를 평가하면 이후의 이러한 함수는 기본 매개 변수의 재평가를 트리거하지 않으므로 새 함수를 인스턴스화하지 않고 항상 이전에 인스턴스화 된 함수를 참조합니다.


첫 질문

  1. 실제로이 진단에 동의하십니까?
  2. 그렇다면이 새로운 행동이 표준에 의해 규정되어 있습니까? 이전 버그였습니까?
  3. 그렇지 않다면 무엇이 문제입니까?

위의 사항을 염두에두고 해결 방법을 next()찾았습니다. 각 호출을 단조롭게 증가하는 고유 ID로 표시하고 호출 수신자에게 전달하여 호출이 동일하지 않도록 컴파일러가 모든 인수를 다시 평가하도록합니다. 매번.

그렇게하는 것은 부담스러운 일이지만, 생각 하면 함수형 매크로에 숨겨져 있는 표준 __LINE__또는 __COUNTER__유사한 매크로를 사용할 수 있습니다 counter_next().

그래서 나는 나중에 이야기 할 문제를 보여주는 가장 단순화 된 형태로 제시하는 다음을 생각해 냈습니다.

template <int N>
struct slot;

template <int N>
struct slot {
    friend constexpr auto counter(slot<N>);
};

template <>
struct slot<0> {
    friend constexpr auto counter(slot<0>) {
        return 0;
    }
};

template <int N, int I>
struct writer {
    friend constexpr auto counter(slot<N>) {
        return I;
    }

    static constexpr int value = I-1;
};

template <int N, typename = decltype(counter(slot<N>()))>
constexpr int reader(int, slot<N>, int R = counter(slot<N>())) {
    return R;
};

template <int N>
constexpr int reader(float, slot<N>, int R = reader(0, slot<N-1>())) {
    return R;
};

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}

int a = next<11>();
int b = next<34>();
int c = next<57>();
int d = next<80>();

위의 결과를 godbolt 에서 볼 수 있습니다 .이 lazies에 대해 스크린 샷을 찍었습니다.

여기에 이미지 설명을 입력하십시오

보시 다시피 7.0.0까지 트렁크 g ++ 및 clang ++을 사용하면 작동합니다! , 카운터는 예상대로 0에서 3으로 증가하지만 7.0.0 이상의 clang ++ 버전에서는 그렇지 않습니다 .

부상에 모욕을 더하기 위해 실제로 카운터에 해당 컨텍스트에 바인딩되도록 카운터에 "context"매개 변수를 추가하여 clang ++을 버전 7.0.0 충돌까지 만들었습니다. 새로운 컨텍스트가 정의 될 때마다 다시 시작되므로 잠재적으로 무한한 양의 카운터를 사용할 수 있습니다. 이 변형을 사용하면 버전 7.0.0 이상의 clang ++은 충돌하지 않지만 여전히 예상 된 결과를 생성하지는 않습니다. godbolt에 산다 .

무슨 일이 일어나고 있는지에 대한 단서를 잃어버린 cppinsights.io 웹 사이트에서 템플릿의 인스턴스화 방법과시기를 확인할 수있었습니다. 내가 생각 하는 그 서비스를 사용하면 clang ++ 은 인스턴스화 될 때마다 실제로 함수를 정의 하지 않습니다 .friend constexpr auto counter(slot<N>)writer<N, I>

counter(slot<N>)이미 인스턴스화해야 할 주어진 N 을 명시 적으로 호출하려고하면 이 가설의 근거가됩니다.

내가 명시 적으로 인스턴스화하려고 경우, writer<N, I>주어진에 대한 N그리고 I그 이미 인스턴스화 할 뻔 ++ 다음 그 소리는 재정의에 대해 불평 friend constexpr auto counter(slot<N>).

위의 내용을 테스트하기 위해 이전 소스 코드에 두 줄을 더 추가했습니다.

int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;

당신은 godbolt 에 자신 위해 모든 것을 볼 수 있습니다 . 아래 스크린 샷.

clang ++은 정의하지 않은 것으로 정의한 것으로 판단

그래서 clang ++은 그것이 정의하지 않은 것으로 정의한 것을 정의했다고 믿습니다 . 어떤 종류의 머리가 회전하지 않습니까?


두 번째 질문

  1. 해결 방법이 합법적 인 C ++ 입니까 , 아니면 다른 g ++ 버그를 발견했을까요?
  2. 합법적이라면 불쾌한 clang ++ 버그를 발견 했습니까?
  3. 아니면 정의되지 않은 행동의 어두운 지하 세계를 탐구 했으므로 나 자신 만 비난 할 수 있습니까?

어쨌든, 나는이 토끼 구멍에서 나와 도움이 필요한 사람을 따뜻하게 환영하고 필요할 경우 헤드 해칭 설명을 제공합니다. :디



2
표준위원회는 사람들이 (가설 적으로) 평가할 때마다 똑같은 결과를 얻지 못하는 모든 종류, 모양 또는 형태의 컴파일 타임 구성을 허용하지 않을 의사가 있음을 분명히 알고 있습니다. 따라서 컴파일러 버그 일 수도 있고, "잘못된 형식의 진단이 필요하지 않은"경우 일 수도 있고 표준에서 놓친 것일 수도 있습니다. 그럼에도 불구하고 그것은 "표준 정신"에 위배됩니다. 미안 해요. 컴파일 타임 카운터도 좋았을 것입니다.
bolov

@HolyBlackCat 나는 그 코드 주위에 머리를 갖기가 매우 어렵다는 것을 고백해야한다고 고백해야합니다. next()함수에 단조 증가하는 숫자를 매개 변수로 명시 적으로 전달할 필요가없는 것처럼 보이지만 실제로는 어떻게 작동하는지 알 수 없습니다. 어쨌든, 나는 내 자신의 문제에 대한 응답을 생각해 냈습니다. stackoverflow.com/a/60096865/566849
Fabio A.

@FabioA. 나도 그 대답을 완전히 이해하지 못합니다. 그 질문을 한 이후로 나는 constexpr 카운터를 다시는 만지고 싶지 않다는 것을 깨달았습니다.
HolyBlackCat

이것은 재미있는 작은 생각 실험이지만 실제로 해당 코드를 사용한 사람은 향후 버전의 C ++에서 작동하지 않을 것으로 예상해야합니다. 그런 의미에서 결과는 스스로를 버그로 정의합니다.
Aziuth

답변:


5

추가 조사 후 next()함수에 수행 할 수있는 사소한 수정이 있으며 , 이로 인해 코드가 7.0.0 이상의 clang ++ 버전에서는 제대로 작동하지만 다른 모든 clang ++ 버전에서는 작동하지 않습니다.

이전 솔루션에서 가져온 다음 코드를 살펴보십시오.

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}

주의를 기울이면 문자 그대로 수행하는 작업은에 연결된 값을 읽고 slot<N>1을 추가 한 다음이 새 값을 동일하게 연결하는 것 slot<N> 입니다.

되면 slot<N>값과 연결되어, 값이 연결되지 slot<Y>로 대신 검색된 Y보다 높은 인덱스를 되 N되도록 slot<Y>연관된 값을 갖는다.

위의 코드의 문제점은 g ++에서 작동하더라도 clang ++ (정확하게 말하겠습니까?)는 연결된 값이 없을 때 반환 된 모든 것을 reader(0, slot<N>()) 영구적으로 반환한다는 것 slot<N>입니다. 이는 모든 슬롯이 기본 값과 효과적으로 연결됨을 의미합니다 0.

해결책은 위의 코드를 다음 코드로 변환하는 것입니다.

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}

공지 사항 slot<N>()으로 수정되었습니다 slot<N-1>(). 의미가 있습니다 :에 값을 slot<N>연결하려면 아직 값이 연결되지 않았으므로 검색하려고 시도하는 것이 의미가 없습니다. 또한 카운터를 늘리고 싶고 관련된 카운터 값에 slot<N>1을 더한 값이되어야 slot<N-1>합니다.

유레카!

그래도 clang ++ 버전 <= 7.0.0이 중단됩니다.

결론

내가 게시 한 원래 솔루션에는 다음과 같은 개념적 버그가있는 것 같습니다.

  • g ++에는 quirk / bug / relaxation이있어 솔루션의 버그를 없애고 결국 코드가 작동하게합니다.
  • clang ++ 버전> 7.0.0은 더 엄격하며 원래 코드의 버그를 좋아하지 않습니다.
  • clang ++ 버전 <= 7.0.0에는 수정 된 솔루션이 작동하지 않는 버그가 있습니다.

이 모든 것을 요약하면 다음 코드는 모든 버전의 g ++ 및 clang ++에서 작동합니다.

#if !defined(__clang_major__) || __clang_major__ > 7
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}
#else
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}
#endif

코드는 그대로 msvc에서도 작동합니다. 사용할 때 ICC 컴파일러는 SFINAE 트리거되지 않습니다 decltype(counter(slot<N>()))할 수 없다는 불평을 선호 deduce the return type of function "counter(slot<N>)"하기 때문에 it has not been defined. 이 버그는 SFINAE를 직접 수행하여 해결할 수 있는 버그 라고 생각합니다counter(slot<N>) . 이것은 다른 모든 컴파일러에서도 작동하지만 g ++는 해제 할 수없는 많은 양의 매우 성가신 경고를 내뱉기로 결정합니다. 따라서이 경우에도 #ifdef구조에 올 수있었습니다.

증거 godbolt에 아래 screnshotted.

여기에 이미지 설명을 입력하십시오


2
나는이 답변이 주제를 닫았다 고 생각하지만 여전히 분석에 옳은지 알고 싶습니다. 따라서 다른 사람이지나 가서 더 나은 단서를 제공하기를 기대하면서 내 대답을 올바른 것으로 받아들이 기 전에 기다릴 것입니다. 또는 확인. :)
Fabio A.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.