web-dev-qa-db-fra.com

Pourquoi std :: unique_ptr :: reset () est-il toujours sans exception?

ne question récente (et surtout ma réponse) m'a fait me demander:

En C++ 11 (et les normes plus récentes), les destructeurs sont toujours implicitement noexcept, sauf indication contraire (c'est-à-dire noexcept(false)). Dans ce cas, ces destructeurs peuvent légalement lever des exceptions. (Notez que c'est toujours un vous devriez vraiment savoir ce que vous faites - genre de situation!)

Cependant, toutes les surcharges de std::unique_ptr<T>::reset() sont toujours déclarées noexcept (voir cppreference ), même si le destructeur si T ne l'est pas, entraînant l'arrêt du programme si un destructeur lève une exception pendant reset(). Des choses similaires s'appliquent à std::shared_ptr<T>::reset().

Pourquoi reset() est-il toujours noexcept, et non conditionnellement noexcept?

Il devrait être possible de le déclarer noexcept(noexcept(std::declval<T>().~T())) ce qui le rend noexcept exactement si le destructeur de T est noexcept. Suis-je en train de manquer quelque chose ici, ou s'agit-il d'un oubli dans la norme (car il s'agit certes d'une situation très académique)?

32
anderas

Les exigences de l'appel à l'objet fonction Deleter sont spécifiques à ce sujet, comme indiqué dans les exigences du membre std::unique_ptr<T>::reset().

De [unique.ptr.single.modifiers]/ , vers N4660 §23.11.1.2.5/3;

unique_ptr Modificateurs

void reset(pointer p = pointer()) noexcept;

Requiert: L'expression get_deleter()(get()) doit être bien formée, avoir un comportement bien défini et ne doit pas lever des exceptions .

En général, le type devrait être destructible. Et selon la cppreference sur le concept C++ Destructible, la norme répertorie cela sous le tableau dans [utility.arg.requirements]/2 =, §20.5.3.1 (c'est moi qui souligne);

Destructible exigences

u.~T() Toutes les ressources détenues par u sont récupérées, aucune exception n'est propagée .

Notez également les exigences générales de la bibliothèque pour les fonctions de remplacement; [res.on.functions]/2 .

25
Niall

std::unique_ptr::reset N'invoque pas directement le destructeur, au lieu de cela, il appelle operator () du paramètre du modèle deleter (qui est par défaut std::default_delete<T>). Cet opérateur est tenu de ne pas lever d'exceptions, comme spécifié dans

23.11.1.2.5 modificateurs unique_ptr [unique.ptr.single.modifiers]

void reset(pointer p = pointer()) noexcept;

Requiert: L'expression get_deleter()(get()) doit être bien formée, avoir un comportement bien défini et ne pas lever d'exceptions.

Notez que ne doit pas lancer n'est pas la même chose que noexcept. operator () de default_delete n'est pas déclaré comme noexcept même s'il n'invoque que l'opérateur delete (exécute l'instruction delete). Cela semble donc être un point plutôt faible de la norme. reset doit être soit conditionnellement noexcept:

noexcept(noexcept(::std::declval<D>()(::std::declval<T*>())))

ou operator () du suppresseur doit être noexcept pour donner une garantie plus longue.

5
VTT

Sans avoir été dans les discussions au sein du comité des normes, ma première pensée est que c'est un cas où le comité des normes a décidé que la douleur de jeter dans le destructeur, qui est généralement considéré comme un comportement indéfini en raison de la destruction de la mémoire de la pile lors du déroulement la pile, n'en valait pas la peine.

Pour le unique_ptr En particulier, considérez ce qui pourrait arriver si un objet détenu par un unique_ptr Jette le destructeur:

  1. La unique_ptr::reset() est appelée.
  2. L'objet à l'intérieur est détruit
  3. Le destructeur jette
  4. La pile commence à se dérouler
  5. Le unique_ptr Sort du cadre
  6. Goto 2

Il y avait des moyens d'éviter cela. L'une consiste à définir le pointeur à l'intérieur du unique_ptr Sur un nullptr avant de le supprimer, ce qui entraînerait une fuite de mémoire, ou de définir ce qui devrait se produire si un destructeur lève une exception dans le cas général .

4
martiert

Il serait peut-être plus facile d'expliquer cela avec un exemple. Si nous supposons que reset n'était pas toujours noexcept, alors nous pourrions écrire du code comme celui-ci causerait des problèmes:

class Foobar {
public:
  ~Foobar()
  {
    // Toggle between two different types of exceptions.
    static bool s = true;
    if(s) throw std::bad_exception();
    else  throw std::invalid_argument("s");
    s = !s;
  }
};

int doStuff() {
  Foobar* a = new Foobar(); // wants to throw bad_exception.
  Foobar* b = new Foobar(); // wants to throw invalid_argument.
  std::unique_ptr<Foobar> p;
  p.reset(a);
  p.reset(b);
}

Que fait-on lorsque p.reset(b) est appelée?

Nous voulons éviter les fuites de mémoire, donc p doit revendiquer la propriété de b afin qu'il puisse détruire l'instance, mais il doit également détruire a qui veut lancer un exception. Alors, comment et nous détruisons à la fois a et b?

De plus, quelle exception doStuff() doit-elle lever? bad_exception Ou invalid_argument?

Forcer reset à toujours être noexcept évite ces problèmes. Mais ce type de code serait rejeté lors de la compilation.

0
OLL