次のいずれかを実行することで、標準ベクトルを拡張して、もう 1 つのメンバーを保持できます。
std::vector<int> v;
v.push_back(1);
また
int os = v.size();
v.resize(os+1);
v[os] = 1;
push_back() を使用したコードの簡潔さ以外に、他に違いはありますか? たとえば、一方が他方よりも効率的か、それとも余分なメモリがそれぞれのケースで異なる方法で割り当てられているか?
push_back
つまり、コピー コンストラクターを呼び出します。
resize
デフォルトのコンストラクターをv[os]
呼び出し、次に代入演算子を呼び出します。
を使用しpush_back
ます。
Lightness が言うように、どちらの場合の割り当ても同等です。詳細については、彼の回答を参照してください。
例を次に示します: http://stacked-crooked.com/view?id=43766666e5c72d282bd94c05e43e8897
しかし、それらは同じ方法でメモリを拡張しますか?
C++ 11では明示的に指定されていませんが(確認しました)、はい。
resize
この場合、「コンテナに追加する」ように定義されており、その唯一の論理的な解釈は、割り当てと同等でinsert
ありpush_back
、割り当てに関しても同様です。
コンテナー実装のバックエンドは、通常、そのメモリ ブロックが拡張されるすべての場合に「必要以上に」割り当てます。これは、標準が義務付けているのではなく、禁止していない標準によって行われており、それを示唆する文言はありません。また、これは当てはまりませんresize
。
最終的には、完全に実装次第ですが、どのメインストリーム コンパイラでも 2 つのアプローチのメモリ割り当て規則に違いがあることに驚かれることでしょう。
このテストケースは、1 つの簡単な例を示しています。
#include <iostream>
#include <vector>
int main() {
std::vector<int> a, b;
for(int i = 0; i != 100; ++i)
a.push_back(0);
for(int i = 0; i != 100; ++i)
b.resize(i+1);
std::cout << a.capacity() << " , " << b.capacity() << std::endl;
}
// Output from my GCC 4.7.2:
// 128 , 128
これは微妙に異なることに注意してください。
int main() {
std::vector<int> a, b;
for(int i = 0; i != 100; ++i)
a.push_back(0);
b.resize(100);
std::cout << a.capacity() << " , " << b.capacity() << std::endl;
}
// Output from my GCC 4.7.2:
// 128 , 100
後者の例ではメモリを異なる増分で拡張しており、メモリ バッキング シーケンス コンテナは倍数で拡張される傾向があるため、これは2 つのアプローチ間の公正なテストではありません。
とにかく、@Pubby が特定しているresize
ように、暗黙の構築に続いて明示的な代入を行う場合、時期尚早に最適化する場合、これは最適ではありません。
現実の世界では、ここでの本当の問題は、仕事に間違った関数を使用し、ばかげたコードを書くことです。
それらは同等ではなく、パフォーマンスに基づいて決定するべきではありませんが、実装によってパフォーマンスが大きく異なる可能性があると言われています。
標準ではpush_back()
、一定の時間を償却する必要があります。これは基本的に、実装が等比級数に従ってオブジェクトのバッファーを拡張する必要があることを意味します (つまり、拡張する場合、新しいサイズは以前のサイズに係数 F > 1 で比例する必要があります)。
にはそのような要件はありませんresize()
。実際のところ、一部の実装では、呼び出しresize()
ているのは、ベクトルの最終的なサイズがどのくらいになるかをよく知っているためであると想定しています。それを念頭に置いて、これらの実装は、等比数列に従うのではなく、要求しているサイズに正確にバッファーを (必要に応じて) 拡張します。つまり、このメカニズムに従って追加するコストは、O(N) ではなく O(N^2) になる可能性がありますpush_back
。
ええ、特にカスタム データ型を使用する場合は、大きな違いが生じる可能性があります。観察:
#include <iostream>
#include <vector>
struct S
{
S()
{
std::cout << "S()\n";
}
S(const S&)
{
std::cout << "S(const S&)\n";
}
S& operator = (const S&)
{
std::cout << "operator =\n";
return *this;
}
~S()
{
std::cout << "~S()\n";
}
};
int main()
{
std::vector<S> v1;
std::cout << "push_back:\n";
v1.push_back(S());
std::vector<S> v2;
std::cout << '\n' << "resize:\n";
v2.resize(1);
v2[0] = S();
std::cout << "\nend\n"; // Ignore destructors after "end" (they're not pertinent to the comparison)
}
出力:
push_back:
S()
S(const S&)
~S()
resize:
S()
S(const S&)
~S()
S()
operator =
~S()
end
~S()
~S()
push_back
ところで。
編集: @Lightness Races in Orbit のコメントに応えて、これはもちろん、ばかげた例にすぎません。S
明らかに本当に「有用な」struct
ではありません。すべての印刷ステートメントを削除するか、単にS
as として定義するとstruct S {};
、もちろんコンパイラーはその多くを最適化できます。
しかし、プログラムで信じられないほど単純なデータ型だけを使用するのはいつからですか? いくつかの単純なデータ型を使用することになりますが、いつか非自明なデータ型も使用する可能性があります。これらの自明でないデータ型には、高価なコンストラクタ、デストラクタ、または代入演算子が含まれている可能性があり (または、それほど高価ではないかもしれませんが、resize
の代わりに繰り返し使用すると合計される可能性がありますpush_back
)、push_back
確かに正しい選択です。
resize()
いくつかの要素を追加する必要がある場合は、メモリをより適切に割り当てることができます。
すなわち
std::vector<int> v;
for(int i = 0; i < n; ++i)
v.push_back(1);
or
int os = v.size();
v.resize(os.size() + n);
for(int i = 0; i < n; ++i)
v[os + i] = 1;
しかし、この場合、使用する方が良いです
v.reserve(os.size() + n);
そして push_back の場合、あなたの場合のように、構築 + 代入も回避されます