そして、もう少し掘り下げたら?
std :: make_shared便利
std :: make_sharedがSTLにまったく表示されたのはなぜですか?
新しく作成されたrawポインターからstd :: shared_ptrを構築すると、メモリリークが発生する可能性がある標準的な例があります。
process(std::shared_ptr<Bar>(new Bar), foo());
プロセス(...)関数の引数を計算するには、次を呼び出す必要があります。
- 新しいバー;
- コンストラクターstd :: shared_ptr;
- foo()。
コンパイラは、たとえば次のように、それらを任意の順序で混合できます。
- 新しいバー;
- foo();
- コンストラクターstd :: shared_ptr。
foo()で例外がスローされると、Barインスタンスのリークが発生します。
次のコード例には潜在的なリークが含まれていません(ただし、後でこの質問に戻ります)。
auto bar = std::shared_ptr<Bar>(new Bar);
auto bar = std::shared_ptr<Bar>(new Bar);
process(bar, foo());
process(std::shared_ptr<Bar>(new Bar));
繰り返します:潜在的なリークが発生するためには、最初の例のようなコードを記述する必要があります-1つの関数が少なくとも2つのパラメーターを取り、そのうちの1つは新しく作成された名前のないstd :: shared_ptrによって初期化され、2番目のパラメーターは別の関数を呼び出すことによって初期化され、例外がスローされる場合があります。
また、潜在的なメモリリークが発生するには、さらに2つの条件が必要です。
- コンパイラが呼び出しを好ましくない方法で混在させるため。
- 2番目のパラメーターを評価する関数が例外をスローするようにします。
このような危険なコードは、std :: shared_ptrの100回の使用で1回よりも頻繁に発生することはありません。
そして、この危険を補うために、std :: shared_ptrはstd :: make_sharedと呼ばれる松葉杖でサポートされていました。
錠剤を少し甘くするために、次の句が標準のstd :: make_shared記述に追加されました。
備考:実装は、メモリ割り当てを1つだけ実行する必要があります。
注:実装では、メモリを1つだけ割り当てる必要があります(SHOULD)。
いいえ、これは保証ではありません。
しかし、cppreferenceは、すべての既知の実装がまさにそれを行うと言います。
このソリューションは、少なくとも2つの割り当てを必要とするコンストラクターを呼び出すことによってstd :: shared_ptrを作成する場合と比較してパフォーマンスを向上させることを目的としています。
std :: make_sharedは役に立たない
c ++ 17以降、std :: make_sharedがSTLに追加されたそのトリッキーな珍しい例でのメモリリークは、もはや可能ではありません。
研究リンク:
- cppreference.comのドキュメント -「until C ++ 17」で検索;
- PVS-Studioでのウサギの穴の深さまたはC ++インタビュー
- cppreference.comの詳細なドキュメント -パラグラフ15。
std :: make_sharedが役に立たないケースが他にもいくつかあります。
std :: make_sharedはプライベートコンストラクターを呼び出すことができません
#include <memory>
class Bar
{
public:
static std::shared_ptr<Bar> create()
{
// return std::make_shared<Bar>(); - no build
return std::shared_ptr<Bar>(new Bar);
}
private:
Bar() = default;
};
int main()
{
auto bar = Bar::create();
return 0;
}
std :: make_sharedはカスタム削除機能をサポートしていません
… variadic template. , , deleter.
std::make_shared_with_custom_deleter…
std::make_shared_with_custom_deleter…
まあ、少なくともコンパイル時にこれらの問題について学びます...
std :: make_sharedは有害です
ランタイムに行きましょう。
オーバーロードされた演算子newおよびoperator deleteは、std :: make_sharedによって無視されます
std::shared_ptr:
std::make_shared:
#include <memory>
#include <iostream>
class Bar
{
public:
void* operator new(size_t)
{
std::cout << __func__ << std::endl;
return ::new Bar();
}
void operator delete(void* bar)
{
std::cout << __func__ << std::endl;
::delete static_cast<Bar*>(bar);
}
};
int main()
{
auto bar = std::shared_ptr<Bar>(new Bar);
// auto bar = std::make_shared<Bar>();
return 0;
}
std::shared_ptr:
operator new
operator delete
std::make_shared:
そして今-記事自体が始まった最も重要なこと。
驚いたことに、事実:std :: shared_ptrがメモリを処理する方法は、メモリの作成方法に大きく依存する可能性があります-std :: make_sharedを使用するか、コンストラクタを使用する!なぜこうなった? std :: make_sharedによって生成される「便利な」単一割り当てには、制御ブロックと管理対象オブジェクト間の追加の接続という形で固有の副作用があるためです。個別に解放することはできません。制御ブロックは、少なくとも1つの弱いリンクがある限り存続する必要があります。 コンストラクタを使用して作成されたstd :: shared_ptrは、次の動作を期待する必要があります。
- 管理対象オブジェクトの割り当て(コンストラクターを呼び出す前、つまりユーザー側)。
- 制御ユニットの割り当て;
- 最後の強力な参照が破棄されると、管理オブジェクトのデストラクタが呼び出され、そのオブジェクトが使用していたメモリが解放されます。同時に単一の弱いリンクがない場合-コントロールユニットのリリース。
- 強いリンクがない場合の最後の弱いリンクの破壊時-コントロールユニットのリリース。
そしてstd :: make_sharedを使用して作成した場合:
- 管理オブジェクトと制御ユニットの割り当て。
- 最後の強力な参照が破棄されると、使用していたメモリを解放せずに管理対象オブジェクトのデストラクタを呼び出します。単一の弱いリンクがない場合は、制御ブロックと管理対象オブジェクトのメモリを解放します。
- 強いリンクがない場合に最後の弱いリンクを破棄するとき-コントロールユニットの解放と管理対象オブジェクトのメモリ。
std :: make_sharedでstd :: shared_ptrを作成すると、スペースリークが発生します。
std :: shared_ptrインスタンスがどのように作成されたかを実行時に正確に区別することは不可能です。
この動作のテストに移りましょう。
非常に単純な方法があります-std :: allocate_sharedをカスタムアロケーターと一緒に使用すると、すべての呼び出しが報告されます。ただし、この方法で取得した結果をstd :: make_sharedに拡張するのは正しくありません。
より正確な方法は、総メモリ消費量を制御することです。しかし、ここではクロスプラットフォームの問題はありません。
Linux用コード、Ubuntu 20.04デスクトップx64でテスト済み。他のプラットフォームでこれを繰り返すことに興味がある人-参照ここを (macOを使用した私の実験では、TASK_BASIC_INFOオプションはメモリの解放を追跡しないことが示されています。TASK_VM_INFO_PURGEABLEがより良い候補です)。
Monitoring.h
#pragma once
#include <cstdint>
uint64_t memUsage();
Monitoring.cpp
#include "Monitoring.h"
#include <fstream>
#include <string>
uint64_t memUsage()
{
auto file = std::ifstream("/proc/self/status", std::ios_base::in);
auto line = std::string();
while(std::getline(file, line)) {
if (line.find("VmSize") != std::string::npos) {
std::string toConvert;
for (const auto& elem : line) {
if (std::isdigit(elem)) {
toConvert += elem;
}
}
return stoull(toConvert);
}
}
return 0;
}
main.cpp
#include <iostream>
#include <array>
#include <numeric>
#include <memory>
#include "Monitoring.h"
struct Big
{
~Big()
{
std::cout << __func__ << std::endl;
}
std::array<volatile unsigned char, 64*1024*1024> _data;
};
volatile uint64_t accumulator = 0;
int main()
{
std::cout << "initial: " << memUsage() << std::endl;
auto strong = std::shared_ptr<Big>(new Big);
// auto strong = std::make_shared<Big>();
std::accumulate(strong->_data.cbegin(), strong->_data.cend(), accumulator);
auto weak = std::weak_ptr<Big>(strong);
std::cout << "before reset: " << memUsage() << std::endl;
strong.reset();
std::cout << "after strong reset: " << memUsage() << std::endl;
weak.reset();
std::cout << "after weak reset: " << memUsage() << std::endl;
return 0;
}
std :: shared_ptrコンストラクターを使用する場合のコンソールへの出力:
初期:5884
リセット前:71424
〜ビッグ
強いリセット後:5884
弱いリセット後:5884
std :: make_shared使用時のコンソールへの出力:
初期:5888
リセット前:71428
〜ビッグ
強いリセット後:71428
弱いリセット後:5888
ボーナス
それでも、コード実行の結果としてメモリをリークすることは可能ですか
auto bar = std::shared_ptr<Bar>(new Bar);
?
Barの割り当ては成功したが、制御ブロックに十分なメモリがない場合はどうなりますか?
コンストラクターがカスタムの削除機能で呼び出された場合はどうなりますか?
標準の[util.smartptr.shared.const]セクションにより、std :: shared_ptrコンストラクター内で例外が発生した場合、次のことが保証されます。
- カスタム削除機能のないコンストラクターの場合、渡されたポインターは、deleteまたはdelete []を使用して削除されます。
- カスタムの削除機能を持つコンストラクターの場合、渡されたポインターはこの同じ削除機能を使用して削除されます。
規格で保証された漏れはありません。
3つのコンパイラ(Apple clangバージョン11.0.3、GCC 9.3.0、MSVC 2019 16.6.2)の実装をざっと読んだ結果、これが事実であることが確認できました。
出力
C ++ 11およびC ++ 14では、std :: make_sharedを使用することによる害は、その単一の有用な関数によって相殺される可能性があります。
c ++ 17以降、算術はstd :: make_sharedを支持しません。
std :: allocate_sharedの場合も同様です。
上記の多くはstd :: make_uniqueにも当てはまりますが、害はほとんどありません。