Et si tu creuses un peu plus?
std :: make_shared utile
Pourquoi std :: make_shared est-il apparu dans STL?
Il existe un exemple canonique où la construction d'un std :: shared_ptr à partir d'un pointeur brut fraîchement créé peut conduire à une fuite de mémoire:
process(std::shared_ptr<Bar>(new Bar), foo());
Pour calculer les arguments de la fonction process (...), vous devez appeler:
- nouveau bar;
- constructeur std :: shared_ptr;
- foo ().
Le compilateur peut les mélanger dans n'importe quel ordre, par exemple comme ceci:
- nouveau bar;
- foo ();
- constructeur std :: shared_ptr.
Si une exception se produit dans foo (), nous obtenons une fuite de l'instance Bar.
Aucun des exemples de code suivants ne contient une fuite potentielle (mais nous reviendrons sur cette question plus tard):
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));
Je le répète: pour qu'une fuite potentielle se produise, il est nécessaire d'écrire un code comme dans le tout premier exemple - une fonction prend au moins deux paramètres, dont l'un est initialisé par le std :: shared_ptr sans nom fraîchement créé, et le deuxième paramètre est initialisé en appelant une autre fonction, ce qui peut lever des exceptions.
Et pour qu'une fuite de mémoire potentielle se matérialise, deux autres conditions sont nécessaires:
- de sorte que le compilateur mélange les appels de manière défavorable;
- de sorte que la fonction évaluant le deuxième paramètre lève réellement une exception.
Il est peu probable qu'un tel code dangereux se produise plus d'une fois sur cent utilisations de std :: shared_ptr.
Et pour compenser ce danger, std :: shared_ptr a été sauvegardé avec une béquille appelée std :: make_shared.
Pour adoucir légèrement la pilule, la phrase suivante a été ajoutée à la description de std :: make_shared dans le Standard:
Remarques: les implémentations ne doivent pas effectuer plus d'une allocation de mémoire.
Remarque: les implémentations DEVRAIENT faire plus d'une allocation de mémoire.
Non, ce n'est pas une garantie.
Mais cppreference dit que toutes les implémentations connues font exactement cela.
Cette solution vise à améliorer les performances par rapport à la création de std :: shared_ptr en appelant un constructeur qui nécessite au moins deux allocations: une pour placer l'objet et la seconde pour contrôler le bloc.
std :: make_shared inutile
À partir de C ++ 17, une fuite de mémoire dans cet exemple rare et délicat pour lequel std :: make_shared a été ajouté à STL n'est plus possible.
Liens d'étude:
- Documentation sur cppreference.com - recherchez "jusqu'à C ++ 17";
- La profondeur d'un terrier de lapin ou une interview en C ++ chez PVS-Studio
- Plus de documentation sur cppreference.com - point 15.
Il existe plusieurs autres cas dans lesquels std :: make_shared est inutile:
std :: make_shared ne pourra pas appeler le constructeur privé
#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 ne prend pas en charge les suppressions personnalisées
… variadic template. , , deleter.
std::make_shared_with_custom_deleter…
std::make_shared_with_custom_deleter…
Eh bien, apprenez au moins ces problèmes au moment de la compilation ...
std :: make_shared est dangereux
Nous passons en runtime.
l'opérateur surchargé new et l'opérateur delete seront ignorés par 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:
Et maintenant - la chose la plus importante pour laquelle l'article lui-même a été lancé.
Étonnamment, le fait: comment std :: shared_ptr va gérer la mémoire peut dépendre de manière significative de la façon dont elle a été créée - en utilisant std :: make_shared ou en utilisant le constructeur!
Pourquoi cela arrive-t-il?
Parce que l'allocation uniforme "utile" produite par std :: make_shared a un effet secondaire inhérent de communication inutile entre le bloc de contrôle et l'objet géré. Ils ne peuvent tout simplement pas être libérés individuellement. Un bloc de contrôle doit vivre tant qu'il y a au moins un maillon faible.
De std :: shared_ptr créé à l'aide du constructeur, vous devez vous attendre au comportement suivant:
- allocation d'un objet géré (avant d'appeler le constructeur, c'est-à-dire du côté utilisateur);
- affectation de l'unité de contrôle;
- lors de la destruction du dernier maillon fort, l'appel du destructeur de l'objet géré et la libération de la mémoire qu'il occupe ; si en même temps il n'y a pas un seul maillon faible - libération de l'unité de contrôle;
- sur la destruction du dernier maillon faible en l'absence de liens forts - libération du bloc de contrôle.
Et si créé avec std :: make_shared:
- allocation de l'objet géré et de l'unité de contrôle;
- lors de la destruction de la dernière référence forte - appeler le destructeur de l'objet géré sans libérer la mémoire qu'il occupe ; s'il n'y a pas en même temps un seul maillon faible - la libération de l'unité de contrôle et de la mémoire de l'objet géré;
- — .
La création de std :: shared_ptr avec std :: make_shared provoque une fuite d'espace.
Il est impossible de distinguer à l'exécution exactement comment l'instance std :: shared_ptr a été créée.
Passons au test de ce comportement.
Il existe un moyen très simple: utilisez std :: allocate_shared avec un allocateur personnalisé, qui rapportera tous les appels. Mais il est incorrect de distribuer les résultats obtenus de cette manière à std :: make_shared.
Une manière plus correcte consiste à contrôler la consommation totale de mémoire. Mais il n'est pas question de multiplateforme.
Code pour Linux, testé sur le bureau Ubuntu 20.04 x64. Qui est intéressé à répéter cela pour d'autres plateformes - voir ici (mes expériences avec macOs ont montré que l'option TASK_BASIC_INFO ne suit pas la libération de mémoire, et TASK_VM_INFO_PURGEABLE est un meilleur candidat).
Surveillance.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;
}
Sortie de la console lors de l'utilisation du constructeur std :: shared_ptr:
initiale: 5884
avant réinitialisation: 71424
~ Grande
après réinitialisation forte: 5884
après réinitialisation faible: 5884
Sortie vers la console lors de l'utilisation de std :: make_shared:
initial: 5888
avant réinitialisation: 71428
~ Gros
après réinitialisation forte: 71428
après réinitialisation faible: 5888
Prime
Pourtant, est-il possible de fuir de la mémoire à la suite de l'exécution de code
auto bar = std::shared_ptr<Bar>(new Bar);
?
Que se passe-t-il si l'allocation de Bar se termine avec succès, mais qu'il n'y a plus assez de mémoire pour le bloc de contrôle?
Et que se passe-t-il si le constructeur avec un deleter personnalisé a été appelé?
La section [util.smartptr.shared.const] de la norme garantit que lorsqu'une exception se produit dans le constructeur std :: shared_ptr:
- pour un constructeur sans suppression personnalisée, le pointeur passé sera supprimé en utilisant delete ou delete [];
- pour un constructeur avec un deleter personnalisé, le pointeur passé sera supprimé en utilisant ce même deleter.
Aucune fuite garantie par la norme.
À la suite d'une lecture rapide des implémentations dans trois compilateurs (Apple clang version 11.0.3, GCC 9.3.0, MSVC 2019 16.6.2), je peux confirmer que c'est le cas.
Production
En c ++ 11 et c ++ 14, le mal d'utiliser std :: make_shared pourrait être compensé par sa seule fonction utile.
Depuis c ++ 17, l'arithmétique n'est pas du tout en faveur de std :: make_shared.
La situation est similaire avec std :: allocate_shared.
Une grande partie de ce qui précède est également vraie pour std :: make_unique, mais cela fait moins de mal.