tl; dr : 내 static_vector에 정의되지 않은 동작이 있다고 생각하지만 찾을 수 없습니다.
이 문제는 Microsoft Visual C ++ 17에서 발생합니다.이 간단하고 완료되지 않은 static_vector 구현, 즉 스택 할당이 가능한 고정 용량을 가진 벡터가 있습니다. 이것은 std :: aligned_storage 및 std :: launder를 사용하는 C ++ 17 프로그램입니다. 문제와 관련이 있다고 생각되는 부분으로 아래에서 정리하려고했습니다.
template <typename T, size_t NCapacity>
class static_vector
{
public:
typedef typename std::remove_cv<T>::type value_type;
typedef size_t size_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T& const_reference;
static_vector() noexcept
: count()
{
}
~static_vector()
{
clear();
}
template <typename TIterator, typename = std::enable_if_t<
is_iterator<TIterator>::value
>>
static_vector(TIterator in_begin, const TIterator in_end)
: count()
{
for (; in_begin != in_end; ++in_begin)
{
push_back(*in_begin);
}
}
static_vector(const static_vector& in_copy)
: count(in_copy.count)
{
for (size_type i = 0; i < count; ++i)
{
new(std::addressof(storage[i])) value_type(in_copy[i]);
}
}
static_vector& operator=(const static_vector& in_copy)
{
// destruct existing contents
clear();
count = in_copy.count;
for (size_type i = 0; i < count; ++i)
{
new(std::addressof(storage[i])) value_type(in_copy[i]);
}
return *this;
}
static_vector(static_vector&& in_move)
: count(in_move.count)
{
for (size_type i = 0; i < count; ++i)
{
new(std::addressof(storage[i])) value_type(move(in_move[i]));
}
in_move.clear();
}
static_vector& operator=(static_vector&& in_move)
{
// destruct existing contents
clear();
count = in_move.count;
for (size_type i = 0; i < count; ++i)
{
new(std::addressof(storage[i])) value_type(move(in_move[i]));
}
in_move.clear();
return *this;
}
constexpr pointer data() noexcept { return std::launder(reinterpret_cast<T*>(std::addressof(storage[0]))); }
constexpr const_pointer data() const noexcept { return std::launder(reinterpret_cast<const T*>(std::addressof(storage[0]))); }
constexpr size_type size() const noexcept { return count; }
static constexpr size_type capacity() { return NCapacity; }
constexpr bool empty() const noexcept { return count == 0; }
constexpr reference operator[](size_type n) { return *std::launder(reinterpret_cast<T*>(std::addressof(storage[n]))); }
constexpr const_reference operator[](size_type n) const { return *std::launder(reinterpret_cast<const T*>(std::addressof(storage[n]))); }
void push_back(const value_type& in_value)
{
if (count >= capacity()) throw std::out_of_range("exceeded capacity of static_vector");
new(std::addressof(storage[count])) value_type(in_value);
count++;
}
void push_back(value_type&& in_moveValue)
{
if (count >= capacity()) throw std::out_of_range("exceeded capacity of static_vector");
new(std::addressof(storage[count])) value_type(move(in_moveValue));
count++;
}
template <typename... Arg>
void emplace_back(Arg&&... in_args)
{
if (count >= capacity()) throw std::out_of_range("exceeded capacity of static_vector");
new(std::addressof(storage[count])) value_type(forward<Arg>(in_args)...);
count++;
}
void pop_back()
{
if (count == 0) throw std::out_of_range("popped empty static_vector");
std::destroy_at(std::addressof((*this)[count - 1]));
count--;
}
void resize(size_type in_newSize)
{
if (in_newSize > capacity()) throw std::out_of_range("exceeded capacity of static_vector");
if (in_newSize < count)
{
for (size_type i = in_newSize; i < count; ++i)
{
std::destroy_at(std::addressof((*this)[i]));
}
count = in_newSize;
}
else if (in_newSize > count)
{
for (size_type i = count; i < in_newSize; ++i)
{
new(std::addressof(storage[i])) value_type();
}
count = in_newSize;
}
}
void clear()
{
resize(0);
}
private:
typename std::aligned_storage<sizeof(T), alignof(T)>::type storage[NCapacity];
size_type count;
};
이것은 한동안 잘 작동하는 것처럼 보입니다. 그런 다음 한 시점에서 이와 비슷한 작업을 수행했습니다. 실제 코드는 더 길지만 요점은 다음과 같습니다.
struct Foobar
{
uint32_t Member1;
uint16_t Member2;
uint8_t Member3;
uint8_t Member4;
}
void Bazbar(const std::vector<Foobar>& in_source)
{
static_vector<Foobar, 8> valuesOnTheStack { in_source.begin(), in_source.end() };
auto x = std::pair<static_vector<Foobar, 8>, uint64_t> { valuesOnTheStack, 0 };
}
즉, 먼저 8 바이트 Foobar 구조체를 스택의 static_vector에 복사 한 다음 8 바이트 구조체의 static_vector의 std :: pair를 첫 번째 멤버로 만들고 uint64_t를 두 번째 멤버로 만듭니다. valuesOnTheStack에 쌍이 생성되기 직전에 올바른 값이 포함되어 있는지 확인할 수 있습니다. 그리고 …이 segfault는 쌍을 구성 할 때 static_vector의 복사 생성자 (호출 함수에 인라인 됨) 내에서 최적화가 활성화되어 있습니다.
간단히 말해, 분해를 검사했습니다. 상황이 조금 이상해집니다. 인라인 복사 생성자 주위에 생성 된 asm이 아래에 나와 있습니다. 이것은 위의 샘플이 아니라 실제 코드에서 가져온 것입니다.
00621E45 mov eax,dword ptr [ebp-20h]
00621E48 xor edx,edx
00621E4A mov dword ptr [ebp-70h],eax
00621E4D test eax,eax
00621E4F je <this function>+29Ah (0621E6Ah)
00621E51 mov eax,dword ptr [ecx]
00621E53 mov dword ptr [ebp+edx*8-0B0h],eax
00621E5A mov eax,dword ptr [ecx+4]
00621E5D mov dword ptr [ebp+edx*8-0ACh],eax
00621E64 inc edx
00621E65 cmp edx,dword ptr [ebp-70h]
00621E68 jb <this function>+281h (0621E51h)
자, 먼저 카운트 멤버를 소스에서 대상으로 복사하는 두 개의 mov 명령이 있습니다. 여태까지는 그런대로 잘됐다. edx는 루프 변수이기 때문에 0이됩니다. 카운트가 0인지 확인합니다. 0이 아니므로 for 루프로 진행하여 먼저 메모리에서 레지스터로, 레지스터에서 메모리로 두 개의 32 비트 mov 연산을 사용하여 8 바이트 구조체를 복사합니다. 그러나 비린내가 있습니다. [ebp + edx * 8 +]와 같은 소스에서 소스 객체를 읽을 수있는 대신 ecx가 있습니다. 소리가 잘 들리지 않습니다. ecx의 가치는 무엇입니까?
ecx에는 가비지 주소가 포함되어 있으며, 우리가 segfaulting하는 것과 동일합니다. 이 값은 어디서 얻었습니까? 바로 위의 asm이 있습니다.
00621E1C mov eax,dword ptr [this]
00621E22 push ecx
00621E23 push 0
00621E25 lea ecx,[<unrelated local variable on the stack, not the static_vector>]
00621E2B mov eax,dword ptr [eax]
00621E2D push ecx
00621E2E push dword ptr [eax+4]
00621E31 call dword ptr [<external function>@16 (06AD6A0h)]
이것은 일반적인 오래된 cdecl 함수 호출처럼 보입니다. 실제로이 함수는 바로 위의 외부 C 함수를 호출합니다. 그러나 무슨 일이 일어나고 있는지 확인하십시오 : ecx는 스택에 인수를 푸시하는 임시 레지스터로 사용되며 함수가 호출됩니다 … 그런 다음 estatic은 소스 static_vector에서 읽기 위해 아래에서 잘못 사용될 때까지 다시 만지지 않습니다.
실제로 ecx의 내용은 여기에서 호출되는 함수로 덮어 쓰게되며 물론 허용됩니다. 그러나 그렇지 않은 경우에도 ecx가 올바른 것에 대한 주소를 포함 할 방법은 없습니다. 최대는 static_vector가 아닌 로컬 스택 멤버를 가리킬 것입니다. 마치 컴파일러가 가짜 어셈블리를 생성 한 것처럼 보입니다. 이 기능은 올바른 출력을 생성 할 수 없습니다 .
그것이 바로 내가 지금있는 곳입니다. std :: launder land에서 놀면서 최적화가 활성화되면 이상한 어셈블리가 정의되지 않은 동작처럼 나에게 냄새가납니다. 그러나 나는 그것이 어디에서 올 수 있는지 알 수 없습니다. 보충적이지만 아주 유용한 정보 인 올바른 플래그가있는 clang은 ecx 대신 ebp + edx를 사용하여 값을 읽는다는 점을 제외하면 이와 유사한 어셈블리를 생성합니다.
답변
컴파일러 버그가 있다고 생각합니다. 추가 __declspec( noinline )
로하는 operator[]
충돌을 해결하는 것 같습니다 :
__declspec( noinline ) constexpr const_reference operator[]( size_type n ) const { return *std::launder( reinterpret_cast<const T*>( std::addressof( storage[ n ] ) ) ); }
버그를 Microsoft에보고 할 수 있지만 Visual Studio 2019에서 이미 수정 된 것으로 보입니다.
제거 std::launder
하면 충돌이 해결되는 것 같습니다.
constexpr const_reference operator[]( size_type n ) const { return *reinterpret_cast<const T*>( std::addressof( storage[ n ] ) ); }