TL; DR
이 전체 게시물을 읽으려고 시도하기 전에 다음 사항을 숙지하십시오.
- 제시된 문제에 대한 해결책 은 나 자신 에 의해 발견 되었지만 여전히 분석이 올바른지 알고 싶어합니다.
fameta::counter
나머지 몇 가지 단점을 해결하는 클래스 로 솔루션을 패키지했습니다 . github에서 찾을 수 있습니다 .- 당신은 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 ++의 문제는 컴파일러가 함수를 처음 호출 할 때 함수 템플릿 기본 매개 변수를 평가하면 이후의 이러한 함수는 기본 매개 변수의 재평가를 트리거하지 않으므로 새 함수를 인스턴스화하지 않고 항상 이전에 인스턴스화 된 함수를 참조합니다.
첫 질문
- 실제로이 진단에 동의하십니까?
- 그렇다면이 새로운 행동이 표준에 의해 규정되어 있습니까? 이전 버그였습니까?
- 그렇지 않다면 무엇이 문제입니까?
위의 사항을 염두에두고 해결 방법을 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 ++은 그것이 정의하지 않은 것으로 정의한 것을 정의했다고 믿습니다 . 어떤 종류의 머리가 회전하지 않습니까?
두 번째 질문
- 해결 방법이 합법적 인 C ++ 입니까 , 아니면 다른 g ++ 버그를 발견했을까요?
- 합법적이라면 불쾌한 clang ++ 버그를 발견 했습니까?
- 아니면 정의되지 않은 행동의 어두운 지하 세계를 탐구 했으므로 나 자신 만 비난 할 수 있습니까?
어쨌든, 나는이 토끼 구멍에서 나와 도움이 필요한 사람을 따뜻하게 환영하고 필요할 경우 헤드 해칭 설명을 제공합니다. :디
답변
추가 조사 후 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.