Question Pourquoi n'y a-t-il aucune alternative sûre à unique_ptr :: operator * ()?


std::vector a la fonction membre at() comme alternative sûre à operator[], afin que la vérification liée soit appliquée et qu'aucune référence ne soit créée:

void foo(std::vector<int> const&x)
{
  const auto&a=x[0];     // What if x.empty()? Undefined behavior!
  const auto&a=x.at(0);  // Throws exception if x.empty().
}

cependant, std::unique_ptr manque la fonctionnalité correspondante:

void foo(std::unique_ptr<int> const&x)
{
  const auto&a=*x;       // What if bool(x)==false? Undefined behavior!
}

Ce serait génial si std::unique_ptr avait une telle alternative sûre, dit membre ref() (et cref()) qui ne retourne jamais une référence en suspens, mais jette plutôt une exception. Mise en oeuvre possible:

template<typename T>
typename add_lvalue_reference<T>::type
unique_ptr<T>::ref() const noexcept(false)
{
  if(bool(*this)==false)
    throw run_time_error("trying to de-refrence null unique_ptr");
  return this->operator*();
}

Y a-t-il une bonne raison pour laquelle la norme ne fournit pas ce genre de chose?


16
2017-08-18 08:22


origine


Réponses:


Je pense que la vraie réponse est simple, et la même pour beaucoup de "Pourquoi le C ++ n'est-il pas comme ça?" des questions:

Personne ne l'a proposé.

std::vector et std::unique_ptr ne sont pas conçus par les mêmes personnes, en même temps, et ne sont pas utilisés de la même manière, donc ne suivez pas nécessairement les mêmes principes de conception.


12
2017-08-18 09:03



unique_ptr a été spécifiquement conçu comme une classe de pointeur légère avec une détection d'état nul (par exemple, indiquée dans optionnel dans Une proposition pour ajouter une classe d'utilitaire pour représenter des objets facultatifs (révision 3))

Cela dit, la capacité que vous demandez est déjà en place depuis opérateur* la documentation indique:

// may throw, e.g. if pointer defines a throwing operator*
typename std::add_lvalue_reference<T>::type operator*() const;

le pointer type est défini comme

std::remove_reference<Deleter>::type::pointer if that type exists, otherwise T*

Par conséquent, grâce à votre programme de suppression personnalisé, vous pouvez effectuer toute opération à la volée, y compris la vérification du pointeur nul et le déclenchement des exceptions.

#include <iostream>
#include <memory>

struct Foo { // object to manage
    Foo() { std::cout << "Foo ctor\n"; }
    Foo(const Foo&) { std::cout << "Foo copy ctor\n"; }
    Foo(Foo&&) { std::cout << "Foo move ctor\n"; }
    ~Foo() { std::cout << "~Foo dtor\n"; }
};

struct Exception {};

struct InternalPtr {
    Foo *ptr = nullptr;
    InternalPtr(Foo *p) : ptr(p) {}
    InternalPtr() = default;

    Foo& operator*() const {
        std::cout << "Checking for a null pointer.." << std::endl;
        if(ptr == nullptr)
            throw Exception();
        return *ptr;
    }

    bool operator != (Foo *p) {
        if(p != ptr)
            return false;
        else
            return true;
    }
    void cleanup() {
      if(ptr != nullptr)
        delete ptr;
    }
};

struct D { // deleter
    using pointer = InternalPtr;
    D() {};
    D(const D&) { std::cout << "D copy ctor\n"; }
    D(D&) { std::cout << "D non-const copy ctor\n";}
    D(D&&) { std::cout << "D move ctor \n"; }
    void operator()(InternalPtr& p) const {
        std::cout << "D is deleting a Foo\n";
        p.cleanup();
    };
};

int main()
{
    std::unique_ptr<Foo, D> up(nullptr, D()); // deleter is moved

    try {
      auto& e = *up;      
    } catch(Exception&) {
        std::cout << "null pointer exception detected" << std::endl;
    }

}

Exemple Live

Pour être complet, je posterai deux alternatives / solutions de rechange supplémentaires:

  1. Pointeur vérifiant un unique_ptr via operator bool

    #include <iostream>
    #include <memory>
    
    int main()
    {
        std::unique_ptr<int> ptr(new int(42));
    
        if (ptr) std::cout << "before reset, ptr is: " << *ptr << '\n';
        ptr.reset();
        if (ptr) std::cout << "after reset, ptr is: " << *ptr << '\n';
    }
    

    (Ce serait probablement la manière la plus clanique de traiter le problème)

  2. Une solution alternative, bien que plus compliquée, est d'utiliser un type de wrapper qui prend en charge la gestion des exceptions


17
2017-08-18 08:40



Je ne peux pas dire pourquoi le comité a décidé de ne pas ajouter une méthode sûre de déréférenciation - la réponse est probablement "parce que ce n'était pas proposé" ou "parce qu'un pointeur brut n'en a pas non plus". Mais il est trivial d'écrire vous-même un modèle de fonction gratuit qui prend n'importe quel pointeur comme argument, le compare à nullptr, puis lance une exception ou retourne une référence à l'objet pointé.

Si vous ne le supprimez pas via un pointeur sur la classe de base, il devrait même être possible de dériver publiquement unique_ptr et ajoutez simplement une telle fonction membre.

Gardez toutefois à l'esprit que l'utilisation d'une telle méthode partout pourrait entraîner un impact significatif sur les performances (identique à). Habituellement, vous voulez valider vos paramètres au plus une fois, pour lesquels une seule instruction if au début est beaucoup mieux adaptée.

Il y a aussi l'école qui dit que vous ne devriez pas lancer d'exceptions en réponse à des erreurs de programmation. Peut-être que le peopke chargé de la conception unique_ptr appartenait à cette école, alors que les concepteurs de vecteurs (beaucoup plus anciens) ne le faisaient pas.


4
2017-08-18 08:54



L'un des principaux objectifs de la conception d'une API de pointeur intelligent est d'être un remplacement instantané avec une valeur ajoutée, pas de pièges ni d'effets secondaires, et un coût proche de zéro. if (ptr) ptr->...est la façon dont l'accès sécurisé au pointeur nu est généralement effectué, la même syntaxe fonctionne bien avec les pointeurs intelligents, ne nécessitant donc aucun changement de code lorsque l'un est remplacé par l'autre.

Une vérification supplémentaire de la validité (par exemple, lancer une exception) placée dans un pointeur interfère avec le prédicteur de branche et peut donc avoir un effet sur les performances, ce qui peut ne plus être considéré comme un remplacement instantané.


3
2017-08-18 08:45



Vous avez

operator bool()

Exemple de: cplusplusreference

// example of unique_ptr::operator bool
#include <iostream>
#include <memory>


int main () {
  std::unique_ptr<int> foo;
  std::unique_ptr<int> bar (new int(12));

  if (foo) std::cout << "foo points to " << *foo << '\n';
  else std::cout << "foo is empty\n";

  if (bar) std::cout << "bar points to " << *bar << '\n';
  else std::cout << "bar is empty\n";

  return 0;
}

unique_ptr est un simple wrapper pour un pointeur brut, inutile de lancer une exception lorsque vous pouvez simplement vérifier facilement une condition booléenne.

Modifier: Apparemment opérateur*  peut jeter.

Des exceptions   1) peut lancer, par ex. si le pointeur définit un opérateur de lancer *

Peut-être que quelqu'un pourrait allumer des lumières pour définir un opérateur de lancer *


2
2017-08-18 08:28



Suite à la suggestion de MikeMB, voici une implémentation possible d’une fonction gratuite pour déréférencer les pointeurs et unique_ptrs pareil.

template<typename T>
inline T& dereference(T* ptr) noexcept(false)
{
  if(!ptr) throw std::runtime_error("attempt to dereference a nullptr");
  return *ptr;
}

template<typename T>
inline T& dereference(std::unique_ptr<T> const& ptr) noexcept(false)
{
  if(!ptr) throw std::runtime_error("attempt to dereference an empty unique_ptr)");
  return *ptr;
}

2
2017-08-18 10:45