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を2番目のメンバーとして作成します。ペアが構成される直前に、valuesOnTheStackに正しい値が含まれていることを確認できます。そして...このsegfaultsは、ペアを構築するときに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)
さて、最初に、カウントメンバーをソースから宛先にコピーする2つのmov命令があります。ここまでは順調ですね。edxはループ変数であるため、ゼロ化されます。次に、countがゼロかどうかを簡単に確認します。これはゼロではないので、forループに進み、最初にメモリからレジスタに、次にレジスタからメモリに、2つの32ビットmov操作を使用して8バイトの構造体をコピーします。しかし、何か怪しいものがあります-[ebp + edx * 8 +]のようなものからのmovがソースオブジェクトから読み取ることが期待される場合、代わりに... [ecx]があります。それは正しく聞こえません。ecxの価値は何ですか?
結局のところ、ecxには、segfaultを実行しているのと同じガベージアドレスのみが含まれています。この値はどこから得たのですか?すぐ上の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は、スタックに引数をプッシュするための一時レジスターとして使用され、関数が呼び出されます...そして、ソースstatic_vectorから読み取るために以下で誤って使用されるまで、ecxは再度タッチされません。
実際には、ecxの内容は、ここで呼び出される関数によって上書きされますが、これはもちろん許可されています。しかし、そうでなかったとしても、ecxが正しいものへのアドレスをここに含むことは決してありません-せいぜい、それはstatic_vectorではないローカルスタックメンバーをポイントします。コンパイラが偽のアセンブリを発行したかのようです。この関数は、正しい出力を生成できませんでした。
それが今の私です。std :: launderランドで遊んでいるときに最適化が有効になっていると、奇妙なアセンブリが定義されていない動作のように臭いがします。しかし、それがどこから来るのかはわかりません。補足的ではあるがわずかに役立つ情報として、正しいフラグを設定したclangは、ecxではなくebp + edxを正しく使用して値を読み取ることを除いて、これと同様のアセンブリを生成します。
is_iterator
)最小限の再現可能な例
clear()
たリソースを呼び出すのはなぜstd::move
ですか?