Existe-t-il des cas d’utilisation valables pour utiliser les nouveaux et supprimer, les pointeurs bruts ou les tableaux de style C avec C ++ moderne?

Voici une vidéo notable ( Arrêtez d’enseigner le C ) sur ce changement de paradigme à enseigner dans le langage c ++.

Et un blog aussi remarquable

J’ai un rêve …

Je rêve de cours / classes / programmes dits C ++ qui cesseront d’enseigner (obligeant) leurs étudiants à utiliser: …

Depuis que C ++ 11 est la norme établie, nous disposons des fonctionnalités de gestion de la mémoire dynamic , également appelées pointeurs intelligents .
Même à partir de normes antérieures, la bibliothèque Containers standard c ++ remplace avantageusement les tableaux bruts (alloués avec le new T[] ) (notamment l’utilisation de std::ssortingng au lieu des tableaux de caractères à terminaison NUL style c).

Question (s) en gras :

Laissons de côté le remplacement de l’emplacement, y a-t-il un cas d’utilisation valable qui ne peut pas être réalisé à l’aide de pointeurs intelligents ou de conteneurs standard mais uniquement à l’aide de new et delete directement (en plus de la mise en œuvre de telles classes conteneur / pointeur intelligent)?

On dit parfois (comme ici ou ici ) que l’utilisation de new et delete handrolled peut être “plus efficace” dans certains cas. Qui sont-ils réellement? Ces cas extrêmes ne doivent-ils pas suivre les allocations de la même manière que les conteneurs standard ou les pointeurs intelligents?

Presque la même chose pour les tableaux bruts de taille fixe de style c: Il existe de nos jours std::array , qui permet tous les types d’affectation, de copie, de référencement, etc., avec la même cohérence syntaxique que tout le monde l’attend. Existe-t-il des cas d’utilisation pour choisir un T myArray[N]; tableau de style c de préférence à std::array myArray; ?


En ce qui concerne les interactions avec les bibliothèques tierces:

Supposé qu’une bibliothèque tierce retourne des pointeurs bruts alloués avec new comme

 MyType* LibApi::CreateNewType() { return new MyType(someParams); } 

vous pouvez toujours envelopper cela dans un pointeur intelligent pour vous assurer que la delete est appelée:

 std::unique_ptr foo = LibApi::CreateNewType(); 

même si l’API nécessite que vous appeliez leur fonction héritée pour libérer la ressource telle que

 void LibApi::FreeMyType(MyType* foo); 

vous pouvez toujours fournir une fonction de suppression:

 std::unique_ptr foo = LibApi::CreateNewType(); 

Je suis particulièrement intéressé par les cas d’utilisation valides “tous les jours” par opposition aux exigences et ressortingctions relatives aux objectives académiques / éducatifs , qui ne sont pas couvertes par les installations standard mentionnées.
Ce new et delete peut être utilisé dans les frameworks de gestion de la mémoire / garbage collection ou la mise en œuvre de conteneur standard est hors de question 1 .


Une motivation majeure …

… poser cette question revient à donner une approche alternative à toutes les questions (de devoirs), qui sont limitées à l’utilisation des constructions mentionnées dans le titre, mais posent de sérieuses questions sur le code de production.

Celles-ci sont souvent appelées les bases de la gestion de la mémoire, ce qui est manifestement faux / incompris par l’OMI comme étant approprié pour les exposés et les tâches des débutants .


1) Ajouter: En ce qui concerne ce paragraphe, cela devrait indiquer clairement que les new et les delete ne sont pas destinées aux étudiants débutants c ++, mais devraient être laissées aux cours plus avancés.

Lorsque la propriété ne devrait pas être locale.

Par exemple, un conteneur de pointeurs peut ne pas vouloir que la propriété de ses pointeurs réside dans les pointeurs eux-mêmes. Si vous essayez d’écrire une liste chaînée avec des ptr uniques uniques, vous pouvez facilement détruire la stack au moment de la destruction.

Un conteneur de type pointeur appartenant à un vector peut mieux convenir au stockage de l’opération de suppression au niveau du conteneur ou du sous-conteneur, et non au niveau de l’élément.

Dans ces cas, et dans des cas similaires, vous encapsulez la propriété comme le fait un pointeur intelligent, mais vous le faites à un niveau supérieur. De nombreuses structures de données (graphiques, etc.) peuvent avoir des problèmes similaires, dans lesquels la propriété réside correctement à un point plus élevé que celui où se trouvent les pointeurs et où elles peuvent ne pas correspondre directement à un concept de conteneur existant.

Dans certains cas, il peut être facile de dissocier la propriété du conteneur du rest de la structure de données. Dans d’autres, ce n’est peut-être pas le cas.

Parfois, vous avez des vies comptées incroyablement complexes non locales. Il n’existe aucun endroit sain pour placer le pointeur de propriété dans ces cas.

Déterminer l’exactitude ici est difficile, mais pas impossible. Des programmes corrects et dotés d’une sémantique complexe sur la propriété existent.


Ce sont tous des cas critiques et peu de programmeurs devraient y être confrontés plus d’une poignée de fois dans une carrière.

Je vais être à contre-courant et dire publiquement «non» (du moins en ce qui concerne la question, je suis presque certaine que vous aviez vraiment l’intention de poser cette question, dans la plupart des cas cités).

Ce qui semble être des cas d’utilisation évidents pour utiliser new et delete (par exemple, la mémoire brute pour un segment de mémoire GC, le stockage pour un conteneur) ne le sont pas. Dans ces cas, vous souhaitez un stockage “brut” et non un object (ou un tableau d’objects, ce que les new et les new[] fournissent respectivement).

Puisque vous voulez un stockage brut, vous avez vraiment besoin / voulez utiliser l’ operator new et l’ operator delete pour gérer le stockage brut lui-même. Vous utilisez ensuite placement new pour créer des objects dans ce stockage brut et appelez directement le destructeur pour détruire les objects. En fonction de la situation, vous pouvez utiliser un niveau d’indirection par rapport à celui-ci. Par exemple, les conteneurs de la bibliothèque standard utilisent une classe Allocator pour gérer ces tâches. Ce paramètre est transmis en tant que paramètre de modèle, qui fournit un sharepoint personnalisation (par exemple, un moyen d’optimiser l’allocation en fonction du modèle d’utilisation typique d’un conteneur particulier).

Ainsi, dans ces situations, vous finissez par utiliser le new mot clé (à la fois dans le nouvel emplacement et dans l’appel de l’ operator new ), mais pas comme T *t = new T[N]; , et c’est ce que je suis sûr que vous aviez l’intention de demander.

Un cas d’utilisation valable consiste à avoir à interagir avec le code hérité. Surtout si on passe des pointeurs bruts à des fonctions qui en prennent possession.

Toutes les bibliothèques que vous utilisez ne peuvent pas utiliser des pointeurs intelligents et pour les utiliser, vous devrez peut-être fournir ou accepter des pointeurs bruts et gérer leur durée de vie manuellement. Cela peut même être le cas dans votre propre base de code si elle a une longue histoire.

Un autre cas d’utilisation est l’interaction avec C qui n’a pas de pointeur intelligent.

Certaines API peuvent s’attendre à ce que vous créiez des objects avec new mais en prendront la propriété. La bibliothèque Qt , par exemple, a un modèle parent-enfant dans lequel le parent supprime ses enfants. Si vous utilisez un pointeur intelligent, vous risquez de rencontrer des problèmes de double suppression si vous ne faites pas attention.

Exemple:

 { // parentWidget has no parent. QWidget parentWidget(nullptr); // childWidget is created with parentWidget as parent. auto childWidget = new QWidget(&parentWidget); } // At this point, parentWidget is destroyed and it deletes childWidget // automatically. 

Dans cet exemple particulier, vous pouvez toujours utiliser un pointeur intelligent et tout ira bien:

 { QWidget parentWidget(nullptr); auto childWidget = std::make_unique(&parentWidget); } 

parce que les objects sont détruits dans l’ordre inverse de la déclaration. unique_ptr supprimera d’abord childWidget , ce qui permettra à childWidget -ci de se parentWidget de parentWidget et d’éviter ainsi la double suppression. Cependant, la plupart du temps, vous n’avez pas cette propreté. Il existe de nombreuses situations dans lesquelles le parent sera détruit en premier et dans ce cas, les enfants seront supprimés deux fois.

Dans le cas ci-dessus, nous détenons la société mère dans cette étendue et contrôlons ainsi totalement la situation. Dans d’autres cas, le parent peut ne pas avoir l’heure, mais nous transférons la propriété de notre widget enfant à ce parent, qui vit ailleurs.

Vous pensez peut-être que pour résoudre ce problème, il vous suffit d’éviter le modèle parent-enfant et de créer tous vos widgets sur la stack, sans parent:

 QWidget childWidget(nullptr); 

ou avec un pointeur intelligent et sans parent:

 auto childWidget = std::make_unique(nullptr); 

Cependant, cela va exploser dans votre visage aussi, car une fois que vous commencez à utiliser le widget, il peut être redevenu parent dans votre dos. Une fois qu’un autre object devient le parent, vous obtenez une double suppression lorsque vous utilisez unique_ptr et une suppression de stack lorsque vous le créez sur la stack.

La façon la plus simple de travailler avec cela est d’utiliser de new méthodes. Tout le rest est soit invitant des problèmes, ou plus de travail, ou les deux.

De telles API peuvent être trouvées dans des logiciels modernes, non obsolètes (comme Qt), et ont été développées il y a des années, bien avant que les pointeurs intelligents ne deviennent une chose. Ils ne peuvent pas être changés facilement car cela casserait le code existant des gens.

Le PO demande spécifiquement comment / quand la manipulation manuelle sera plus efficace dans un cas d’utilisation quotidienne – et je vais répondre à cela.

En supposant un compilateur / stl / plate-forme moderne, il n’existe pas une utilisation quotidienne où l’utilisation manuelle de new et de delete sera plus efficace. Pour le cas shared_ptr, je pense que ce sera marginal. Dans une (des) boucle (s) extrêmement serrée (s), il pourrait être avantageux d’utiliser simplement raw new pour éviter le comptage des arbitres (et trouver une autre méthode de nettoyage – à moins que cela ne vous soit imposé, vous choisissez d’utiliser shared_ptr pour une raison quelconque), mais ce n’est pas un exemple quotidien ou commun. En ce qui concerne unique_ptr, il n’y a pas vraiment de différence, je pense donc qu’il est prudent de dire qu’il s’agit davantage de rumeurs et de folklore et que, du sharepoint vue des performances, cela n’aura aucune importance (la différence ne sera pas mesurable dans les cas normaux).

Il existe des cas où il n’est pas souhaitable ou possible d’utiliser une classe de pointeur intelligent comme déjà couverte par d’autres.

Pour les cas d’utilisation simples, les pointeurs intelligents, les conteneurs standard et les références doivent être suffisants pour ne pas utiliser de pointeurs, ni d’allocation ni de désaffectation brutes.

Maintenant, pour les cas auxquels je peux penser:

  • développement de conteneurs ou d’autres concepts de bas niveau – après tout, la bibliothèque standard elle-même est écrite en C ++ et utilise des pointeurs bruts, new et delete
  • optimisation de bas niveau. Cela ne devrait jamais être une préoccupation de première classe, car les compilateurs sont suffisamment intelligents pour optimiser le code standard , et la maintenabilité est normalement plus importante que les performances brutes. Mais lorsque le profilage montre qu’un bloc de code représente plus de 80% du temps d’exécution, l’optimisation de bas niveau est logique et c’est l’une des raisons pour lesquelles la bibliothèque standard de bas niveau C fait toujours partie des standards C ++.

Un autre cas d’utilisation valide est le codage d’un ramasse-miettes .

Imaginez que vous codiez un interpréteur Scheme en C ++ 11 (ou un interpréteur Ocaml bytecode). Ce langage nécessite de coder un CPG (vous devez donc en coder un en C ++). Donc, la propriété n’est pas locale, a répondu Yakk . Et vous voulez récupérer des valeurs de schéma, pas de mémoire brute!

Vous finirez probablement par utiliser explicitement new et delete .

En d’autres termes, les pointeurs intelligents C ++ 11 favorisent un schéma de comptage de références . Mais c’est une technique de GC médiocre (elle n’est pas conviviale avec les références circulaires, qui sont courantes dans Scheme).

Par exemple, une façon naïve d’implémenter un GC simple à balayer en balais serait de rassembler dans un conteneur global tous les pointeurs de valeurs Scheme, etc.

Lisez aussi le manuel du GC .

Lorsque vous devez passer quelque chose à travers la limite de la DLL. Vous ne pouvez (presque) pas faire cela avec des pointeurs intelligents.

3 exemples courants où vous devez utiliser new au lieu de make_... :

  • Si votre object n’a pas de constructeur public
  • Si vous voulez utiliser un deleter personnalisé
  • Si vous utilisez c ++ 11 et souhaitez créer un object géré par un unique_ptr (même si je vous recommande d’écrire votre propre make_unique dans ce cas).

Cependant, dans tous ces cas, vous allez directement envelopper le pointeur renvoyé dans un pointeur intelligent.

2 à 3 exemples (probablement pas si courants), dans lesquels vous ne voudriez pas / ne pouvez pas utiliser les pointeurs intelligents:

  • Si vous devez passer vos types via une c-api (vous êtes celui qui implémente create_my_object ou qui implémente un rappel qui doit prendre un void *)
  • Cas de propriété conditionnelle: Pensez à une chaîne, qui n’alloue pas de mémoire lorsqu’elle est créée à partir d’une chaîne littérale, mais pointe uniquement sur ces données. Nowerdays, vous pourriez probablement utiliser std::variant> , mais uniquement si vous êtes d’accord avec les informations relatives à la propriété stockée dans la variante et si vous acceptez le surcoût de la vérification du membre. actif pour chaque access. Bien sûr, cela n’est pertinent que si vous ne pouvez pas / ne voulez pas vous permettre le surcoût de deux pointeurs (un propriétaire et un non-propriétaire)
    • Si vous souhaitez baser votre propriété sur quelque chose de plus complexe qu’un pointeur. Par exemple, vous voulez utiliser un propriétaire gsl :: afin de pouvoir facilement interroger sa taille et disposer de tous les autres avantages (itération, rangecheck …). Certes, il est fort probable que vous incluez cela dans votre propre classe. Cela pourrait donc entrer dans la catégorie de la mise en œuvre d’un conteneur.

Vous devez parfois appeler new lorsque vous utilisez des constructeurs privés.

Supposons que vous décidiez d’avoir un constructeur privé pour un type destiné à être appelé par une fabrique d’amis ou une méthode de création explicite. Vous pouvez appeler new dans cette fabrique, mais make_unique ne fonctionnera pas.

En ajoutant à d’autres réponses, il y a des cas où nouveau / supprimer a un sens –

  1. Intégration avec une bibliothèque tierce qui renvoie le pointeur brut et attend que vous le renvoyiez à la bibliothèque une fois que vous avez terminé (la bibliothèque possède sa propre fonctionnalité de gestion de la mémoire).
  2. Travailler sur un périphérique intégré aux ressources limitées où la mémoire (RAM / ROM) est un luxe (même quelques kilo-octets). Êtes-vous sûr de vouloir append plus de mémoire de runtime (RAM) et compilée (ROM / Surimpression) à votre application ou voulez-vous programmer soigneusement avec nouveau / supprimer?
  3. D’un sharepoint vue puriste, dans certains cas, les pointeurs intelligents ne fonctionneront pas de manière intuitive (en raison de leur nature). Par exemple, pour le modèle de générateur, vous devez utiliser reinterpret_pointer_cast, si vous utilisez des pointeurs intelligents. Un autre cas est celui où vous devez convertir un type de base en un type dérivé. Vous vous mettez en danger si vous obtenez le pointeur brut du pointeur intelligent, le diffusez et le placez dans un autre pointeur intelligent et finissez par le libérer plusieurs fois.

L’un des problèmes que je traite est celui de l’exploitation de structures de données volumineuses pour la conception de matériel et l’parsing linguistique comportant quelques centaines de millions d’éléments. L’utilisation de la mémoire et les performances sont une considération.

Les conteneurs sont un moyen pratique et pratique d’assembler rapidement des données et de les utiliser, mais la mise en œuvre utilise de la mémoire supplémentaire et des déréférences supplémentaires, qui affectent à la fois la mémoire et les performances. Mon expérience récente consistant à remplacer des pointeurs intelligents par une implémentation personnalisée différente a permis d’obtenir un gain de performances d’environ 20% dans un préprocesseur Verilog. Il y a quelques années, j’ai comparé des listes personnalisées et des arbres personnalisés à des vecteurs / cartes et constaté également des gains. Les implémentations personnalisées reposent sur de nouvelles opérations / suppressions régulières

Ainsi, new / delete sont utiles dans les applications à haute efficacité pour les structures de données conçues sur mesure.

Vous pouvez toujours utiliser new et delete si nous voulons créer notre propre mécanisme d’allocation de mémoire léger. Par exemple

1.Utilisation sur place nouveau: généralement utilisé pour l’allocation de mémoire préallouée;

 char arr[4]; int * intVar = new (&arr) int; // assuming int of size 4 bytes 

2.Utilisation d’allocateurs spécifiques à une classe: Si nous voulons un allocateur personnalisé pour nos propres classes.

 class AwithCustom { public: void * operator new(size_t size) { return malloc(size); } void operator delete(void * ptr) { free(ptr); } }; 

Le cas d’utilisation principal où j’utilise encore des pointeurs bruts est lors de la mise en œuvre d’une hiérarchie qui utilise des types de retour covariants .

Par exemple:

 #include  #include  class Base { public: virtual ~Base() {} virtual Base* clone() const = 0; }; class Foo : public Base { public: ~Foo() override {} // Case A in main wouldn't work if this returned `Base*` Foo* clone() const override { return new Foo(); } }; class Bar : public Base { public: ~Bar() override {} // Case A in main wouldn't work if this returned `Base*` Bar* clone() const override { return new Bar(); } }; int main() { Foo defaultFoo; Bar defaultBar; // Case A: Can maintain the same type when cloning std::unique_ptr fooCopy(defaultFoo.clone()); std::unique_ptr barCopy(defaultBar.clone()); // Case B: Of course cloning to a base type still works std::unique_ptr base1(fooCopy->clone()); std::unique_ptr base2(barCopy->clone()); return 0; } 

Il est encore possible d’utiliser malloc/free en C ++, car vous pouvez utiliser new/delete et tout ce qui est plus avancé en STL les modèles de mémoire STL fournis.

Je pense que pour vraiment apprendre le C ++ et surtout comprendre les modèles de mémoire C ++ 11, vous devez créer des structures simples avec new et delete . Juste pour mieux comprendre comment ils fonctionnent. Toutes les classes de pointeurs intelligents reposent sur ces mécanismes. Donc, si vous comprenez ce que font les new et les delete , vous apprécierez davantage le modèle et trouverez des moyens intelligents de les utiliser.

Aujourd’hui, j’essaie personnellement de les éviter autant que possible, mais l’une des principales raisons est la performance, qui doit être considérée comme essentielle.

Ce sont mes règles empiriques que j’ai toujours à l’esprit:

std::shared_ptr : Gestion automatique des pointeurs, mais en raison du comptage des références utilisé pour suivre les pointeurs auxquels vous avez accédé, les performances sont pires chaque fois que vous accédez à ces objects. Comparé simples, je dirais 6 fois plus lent. N’oubliez pas que vous pouvez utiliser get() , extraire le pointeur primitif et continuer à y accéder. De vous devez être prudent avec celui-là. J’aime cela comme une référence avec *get() , donc la pire performance n’est pas vraiment une affaire.

std::unique_ptr L’access au pointeur ne peut avoir lieu qu’en un point du code. Parce que ce modèle interdit la copie, grâce à la fonctionnalité r-references && , il est beaucoup plus rapide qu’un std::shared_ptr . Comme il y a encore des frais de propriété dans cette classe, je dirais, ils sont environ deux fois plus lents qu’un pointeur primitif. Vous accédez à l’object par rapport au pointeur primitif de ce modèle. J’aime aussi utiliser l’astuce de référence ici, pour des access moins requirejs à l’object.

En ce qui concerne les performances, il est peut-être vrai que ces modèles sont plus lents, mais gardez à l’esprit que si vous souhaitez optimiser les logiciels, vous devez commencer par profiler et voir ce qui prend vraiment de nombreuses instructions. Il est très peu probable que le problème provienne de pointeurs intelligents, mais cela dépend certainement de votre implémentation.

En C ++, personne ne devrait se soucier de malloc et de free , mais ils existent pour le code existant. Ils diffèrent fondamentalement par le fait qu’ils ne savent rien des classes c ++, ce qui, avec les opérateurs new et delete , est différent.

J’utilise std::unique_ptr et std::shared_ptr dans mon projet Commander Genius partout et je suis vraiment heureux qu’ils existent. Je n’ai pas eu à faire face à des memory leaks ni à des segfaults depuis lors. Avant cela, nous avions notre propre modèle de pointeur intelligent. Donc, pour les logiciels productifs, je ne peux pas les recommander assez.

Un autre cas d’utilisation peut être une bibliothèque tierce renvoyant un pointeur brut qui est couvert en interne par son propre comptage de références intrusives (ou par sa propre gestion de la mémoire – qui n’est couverte par aucune interface API / utilisateur).

OpenSceneGraph et son implémentation du conteneur osg :: ref_ptr et de la classe de base osg :: Referenced sont un bon exemple.

Bien qu’il soit possible d’utiliser shared_ptr, le comptage de références intrusives est bien meilleur pour les graphes de scène, comme les cas d’utilisation.

Personnellement, je ne vois rien d’intelligent dans le fichier unique_ptr. C’est juste scope verrouillé nouveau et supprimer. Bien que shared_ptr semble beaucoup mieux, il nécessite des frais généraux inacceptables dans de nombreux cas.

Donc, en général, mon cas d’utilisation est:

Lorsque vous traitez avec des wrappers de pointeur brut non-STL.

Un autre exemple qui n’a pas déjà été mentionné concerne le moment où vous devez passer un object via un rappel C hérité (éventuellement asynchrone). Habituellement, ces éléments prennent un pointeur de fonction et un vide * (ou un descripteur opaque) sur lesquels passer des données utiles. Tant que le rappel donne une certaine garantie sur quand, comment et combien de fois il sera invoqué, le recours à un simple nouveau-> cast-> callback-> cast-> delete est la solution la plus simple (ok, la suppression sera probablement géré par un unique_ptr sur le site de rappel, mais le nouveau nu est toujours là). Bien sûr, des solutions alternatives existent, mais nécessitent toujours la mise en œuvre d’une sorte de “gestionnaire de durée de vie d’object” explicite / implicite dans ce cas.

Je pense que c’est généralement un bon cas d’utilisation et / ou une bonne directive à suivre:

  • Lorsque le pointeur est local à la scope d’une fonction unique.
  • La mémoire dynamic est gérée dans la fonction et vous avez besoin du tas.
  • Vous ne faites pas passer le pointeur et il ne quitte pas la scope des fonctions.

Code PSEUDO:

 #include  // Texture is a class or struct defined somewhere else. unsigned funcToOpenAndLoadImageData( const std::ssortingng& filenameAndPath, Texture& texture, some optional flags (how to process or handle within function ) { // Depending on the above library: file* or iostream... // 1. OpenFile // 2. Read In Header // 3. Process Header // 4. setup some local variables. // 5. extract basic local variables from the header // A. texture width, height, bits per pixel, orientation flags, compression flags etc. // 6. Do some calculations based on the above to find out how much data there is for the actual ImageData... // 7. Raw pointer (typically of unsigned char). // 8. Create dynamic memory for that pointer or array. // 9. Read in the information from the file of that amount into the pointer - array. // 10. Verify you have all the information. // 11. Close the file handle. // 12. Process some more information on the actual pointer or array itself // based on its orientation, its bits per pixel, its dimensions, the color type, the compression type, and or if it exists encryption type. // 13. Store the modified data from the array into Your Structure (Texture - Class/Struct). // 14. Free up dynamic memory... // 15. typically return the texture through the parameter list as a reference // 16. typically return an unsigned int as the Texture's numerical ID. } 

C’est assez efficace. efficace, ne nécessite aucune utilisation de pointeurs intelligents; est rapide surtout si la fonction est en ligne. Ce type de fonction peut être autonome ou même membre d’une classe. If a pattern follows this then it is quite safe to use new & delete or new[] & delete[] if done properly.

MODIFIER

In the mentioned case(s) above sometimes you want the raw pointers and you want it on the heap. Let’s say you have an application that will load say 5,000 texture files, 500 model files, 20 scene files, 500-1000 audio files. You do not want your loading time to be slow, you also want it to be “cache” friendly. Texture loading is very good example of having the pointer on the heap as opposed to the functions stack because the texture could be large in size exceeding your local memory capabilities.

In this context you will be calling these load functions once per object, but you will be calling them several times. After you loaded & created your resources or assets and stored them internally is when and where you would want to use containers instead of arrays and smart pointers instead of raw pointers.

You will load a single asset once, but you may have 100s or 1000s of instances of it. It is with these instances that you would prefer the use of containers and the use of smart pointers to manage their memory within your application over raw pointers and arrays. The initial loading is where you would prefer to be closer to the metal without all the extra unwanted overhead.

If you were working on a A+ class game and you could save your audience 15 to 30s or more of loading time per loading screen then you are in the winners circle. Yes care does need to be taken and yes you can still have unhandled exceptions, but no code is 100% full proof.

This type of design is rarely prone to memory leaks except for those exceptions which can still be handled in many of the cases. Also to safely manage raw pointers, preprocessor macros work well for easy clean up.

Many of these library types also work and deal with raw data , raw memory allocation , etc. and many times smart pointers don’t necessarily fit these types of jobs.

When you want to create multidimensional arrays but aren’t familiar with C++11 syntax like std::move, or aren’t familiar with writing custom deleters for smart pointers.