Mini-guide de survie pour les utilisateurs·trices de unique_ptr

Michel SIMATIC

11 avril 2023

1 Introduction

Lors de l’étape 2 de l’exercice Outil de visualisation, il se peut que vous ayez rencontré ou non des soucis à convertir vos pointeurs nus en unique_ptr.

Les paragraphes suivants présentent quelques soucis classiques lors de l’utilisation des unique_ptravec les symptômes, des propositions de solution et un exemple pour faire apparaître le souci.

2 Tentative de copie directe d’un unique_ptr

Dans le cas où votre code tente de faire une copie directe d’un unique_ptr, la compilation de votre code vous affiche un message comme :

.../unitTests/unitTests.cpp:70:19: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = Group; _Dp = std::default_delete<Group>]’
   70 |     auto pg2 { pg };
...\unitTests\unitTests.cpp(70): error C2280: 'std::unique_ptr<Group,std::default_delete<Group>>::unique_ptr(const std::unique_ptr<Group,std::default_delete<Group>> &)' : tentative de référencement d'une fonction supprimée

Allez voir la ligne de votre source indiquée par le compilateur (par exemple, ligne 70 de unitTests.cpp dans les 2 messages ci-dessus) : Elle contient une copie d’un unique_ptr.

Voici quelques pistes de solutions (à vous de trouver celle qui convient à votre code !) :

Voici un exemple pour faire apparaître le souci. Dans votre fichier unitTests.cpp, ajoutez #include <memory> et le test suivant :

TEST(TestAnalyseXML, Test_copy_unique_ptr) {
    std::string s = R"(<?xml version = "1.0"?>
                       <Group label="testGroupHybrid" x="0" y="1">
                            <Circle label="testCircle" x="2" y="3" r="4" color="Black"/>
                            <Group label="testGroup" x="5" y="6"/>
                       </Group>)";
    pugi::xml_document doc;
    pugi::xml_parse_result result = doc.load_string(s.c_str());
    ASSERT_TRUE(result) << result.description(); // Si jamais result est faux, indique que le test est faux *et* affiche la string result.description() (qui contient la raison de l'erreur)
    auto pg = std::make_unique<Group>( doc.child("Group") );
    auto pg2 { pg };
    std::string g_dump_ref =
            R"(Group "testGroupHybrid", x: 0, y: 1, children: [
| Circle "testCircle", x: 2, y: 3, r: 4, color: "Black"
| Group "testGroup", x: 5, y: 6, children: [
| ]
]
)";
    EXPECT_EQ(pg->dump(), g_dump_ref);
}

Si vous essayez de compiler, vous devriez avoir un message d’erreur similaire au message ci-dessus, ce qui vous permet de détecter que le souci vient de l’instruction auto pg2 { pg }; qui cherche à créer un unique_ptr pg2 en faisant une copie du unique_ptr pg, ce qui est interdit.

Nous avons vu précédemment 3 pistes de solutions. Toutefois, pour pouvoir illustrer un autre souci classique avec les unique_ptr (cf. section suivante), nous vous proposons de corriger le souci en utilisant std::move() et donc en remplaçant auto pg2 { pg }; par auto pg2 { std::move(pg) };

3 Tentative d’utilisation d’un unique_ptr qu’on s’est engagé à ne plus utiliser avec std::move()

Dans le cas où votre code tente d’utiliser un unique_ptr qu’il s’est engagé à ne plus utiliser (votre code a fait un std::move() sur ce unique_ptr), votre code compile, mais vous aurez un plantage à l’exécution au niveau de la ligne qui utilise le unique_ptr qui a subi un std::move().

NB : Avec l’IDE Clion (et avec le compilateur Clang ?), l’IDE vous affiche un warning “Clang-Tidy: ‘nom de votre unique_ptr’ used after it was moved”.

Voici quelques pistes de solutions (à vous de trouver celle qui convient à votre code !) :

Par exemple, si vous exécutez le code ajouté à la section précédente, votre programme plantera. Le débugger vous permettra de voir que c’est au niveau de la ligne EXPECT_EQ(pg->dump(), g_dump_ref); qui travaille avec pg sur lequel vous avez fait std::move(pg) quelques lignes plus haut.

Pour corriger le problème, vous pouvez :

4 Tentative de copie cachée d’un unique_ptr

Parfois, la tentative de copie d’un unique_ptr ne se fait pas de manière directe, mais par exemple en tentant de copier un objet dont une donnée membre est un unique_ptr. Un tel objet n’est pas copiable, mais hélas le message des compilateurs à ce sujet n’est pas forcément des plus utiles. Ils indiquent généralement qu’une tentative de copie a eu lieu, mais sans dire où. Une astuce pour forcer le compilateur à être plus explicite consiste à interdire explicitement la copie de la classe. Il vous indiquera alors l’emplacement des tentatives de copie. Parmi les messages d’erreurs initiaux du compilateur, repérez celui où il évoque une de vos classes : C’est la classe pour laquelle votre code fait une copie d’instance, ce qui entraîne une tentative de duplication du unique_ptr qu’elle contient.

Pour retrouver la ligne à laquelle se passe cette copie, ajoutez la définition suivante (qui indique que l’appel au constructeur de copie de MaClasse est interdit) à MaClasse.h :

        MaClasse(const MaClasse &) = delete;

Relancez la génération de votre exécutable : Le compilateur affiche désormais un message d’erreur mentionnant explicitement la ligne à laquelle se passe votre copie.

Voici quelques pistes de solutions (à vous de trouver celle qui convient à votre code !) :

Voici un exemple pour faire apparaître le souci. Dans votre fichier unitTests.cpp, ajoutez #include <memory> et le test suivant :

TEST(TestAnalyseXML, Test_hidden_copy_unique_ptr) {
    std::string s = R"(<?xml version = "1.0"?>
                       <Group label="testGroupHybrid" x="0" y="1">
                            <Circle label="testCircle" x="2" y="3" r="4" color="Black"/>
                            <Group label="testGroup" x="5" y="6"/>
                       </Group>)";
    pugi::xml_document doc;
    pugi::xml_parse_result result = doc.load_string(s.c_str());
    ASSERT_TRUE(result) << result.description(); // Si jamais result est faux, indique que le test est faux *et* affiche la string result.description() (qui contient la raison de l'erreur)
    Group g{ doc.child("Group") };
    Group g2{ g };
    std::string g_dump_ref =
            R"(Group "testGroupHybrid", x: 0, y: 1, children: [
| Circle "testCircle", x: 2, y: 3, r: 4, color: "Black"
| Group "testGroup", x: 5, y: 6, children: [
| ]
]
)";
    EXPECT_EQ(g.dump(), g_dump_ref);
}

Si vous essayez de compiler, vous obtenez de nombreux messages dont voici un extrait :

In file included from /usr/include/c++/11/memory:66,
                 from .../cmake-build-debug/unitTests/googletest-src/googletest/include/gtest/gtest.h:56,
                 from .../unitTests/unitTests.cpp:1:
/usr/include/c++/11/bits/stl_uninitialized.h: In instantiation of ‘_ForwardIterator std::uninitialized_copy(_InputIterator, _InputIterator, _ForwardIterator) [with _InputIterator = __gnu_cxx::__normal_iterator<const std::unique_ptr<Shape>*, std::vector<std::unique_ptr<Shape> > >; _ForwardIterator = std::unique_ptr<Shape>*]’:
/usr/include/c++/11/bits/stl_uninitialized.h:333:37:   required from ‘_ForwardIterator std::__uninitialized_copy_a(_InputIterator, _InputIterator, _ForwardIterator, std::allocator<_Tp>&) [with _InputIterator = __gnu_cxx::__normal_iterator<const std::unique_ptr<Shape>*, std::vector<std::unique_ptr<Shape> > >; _ForwardIterator = std::unique_ptr<Shape>*; _Tp = std::unique_ptr<Shape>]’
/usr/include/c++/11/bits/stl_vector.h:558:31:   required from ‘std::vector<_Tp, _Alloc>::vector(const std::vector<_Tp, _Alloc>&) [with _Tp = std::unique_ptr<Shape>; _Alloc = std::allocator<std::unique_ptr<Shape> >]’
.../src/./Group.h:6:7:   required from here
/usr/include/c++/11/bits/stl_uninitialized.h:138:72: error: static assertion failed: result type must be constructible from value type of input range
  138 |       static_assert(is_constructible<_ValueType2, decltype(*__first)>::value,
      |                                                                        ^~~~~
/usr/include/c++/11/bits/stl_uninitialized.h:138:72: note: ‘std::integral_constant<bool, false>::value’ evaluates to false
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\vector(745): note: pendant la compilation de la fonction membre classe modèle 'std::vector<std::unique_ptr<Shape,std::default_delete<Shape>>,std::allocator<std::unique_ptr<Shape,std::default_delete<Shape>>>>::vector(const std::vector<std::unique_ptr<Shape,std::default_delete<Shape>>,std::allocator<std::unique_ptr<Shape,std::default_delete<Shape>>>> &)'
  ...\src\Group.h(14): note: voir la référence à l'instanciation de la fonction modèle 'std::vector<std::unique_ptr<Shape,std::default_delete<Shape>>,std::allocator<std::unique_ptr<Shape,std::default_delete<Shape>>>>::vector(const std::vector<std::unique_ptr<Shape,std::default_delete<Shape>>,std::allocator<std::unique_ptr<Shape,std::default_delete<Shape>>>> &)' en cours de compilation
  ...\src\Group.h(13): note: voir la référence à l'instanciation classe modèle 'std::vector<std::unique_ptr<Shape,std::default_delete<Shape>>,std::allocator<std::unique_ptr<Shape,std::default_delete<Shape>>>>' en cours de compilation
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\xutility(261): error C2280: 'std::unique_ptr<Shape,std::default_delete<Shape>>::unique_ptr(const std::unique_ptr<Shape,std::default_delete<Shape>> &)' : tentative de référencement d'une fonction supprimée
  C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\memory(3216): note: voir la déclaration de 'std::unique_ptr<Shape,std::default_delete<Shape>>::unique_ptr'
  C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\memory(3216): note: 'std::unique_ptr<Shape,std::default_delete<Shape>>::unique_ptr(const std::unique_ptr<Shape,std::default_delete<Shape>> &)' : la fonction a été supprimée explicitement
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\xmemory(680): error C2280: 'std::unique_ptr<Shape,std::default_delete<Shape>>::unique_ptr(const std::unique_ptr<Shape,std::default_delete<Shape>> &)' : tentative de référencement d'une fonction supprimée
  C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\memory(3216): note: voir la déclaration de 'std::unique_ptr<Shape,std::default_delete<Shape>>::unique_ptr'
  C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\memory(3216): note: 'std::unique_ptr<Shape,std::default_delete<Shape>>::unique_ptr(const std::unique_ptr<Shape,std::default_delete<Shape>> &)' : la fonction a été supprimée explicitement
 

Quel que soit le compilateur, les messages évoquent Group.h. Donc, nous modifions Group.h en ajoutant :

        Group(const Group &) = delete;

En relançant la compilation, le compilateur affiche désormais le message :

...\unitTests\unitTests.cpp(70): error C2280: 'Group::Group(const Group &)' : tentative de référencement d'une fonction supprimée

Cela vous permet de retrouver l’instruction qui pose souci : Group g2{ g };

NB :

  1. Si vous corrigez le souci en écrivant Group g2{ std::move(g) };, l’instruction EXPECT_EQ(g.dump(), g_dump_ref); qui suit ne fera pas planter votre programme à l’exécution (comme cela arrivait lors du std::move() d’un unique_ptr).
  2. L’IDE CLion émet un warning Clang-Tidy: 'g' used after it was moved