Cette implémentation complète du type optional est similaire à std::optional introduit dans C++17.
Structure générale
template<class T>
struct optional{
private:
T value;
bool has_value;
public:
optional(T value) : value(value),has_value(has_value){}
T value(){return value;}
bool has_value(){return has_value;}
};
La structure ci-dessus cosntitue une version basique de optional, permettant de vérifier la présence ou l'absence d'une valeur et d'en extraire le contenu.
Cependant, elle est trop simpliste et néglige plusieurs aspects importants. Par exemple, lorsque le type T est spécifié, la valeur est construite dès la création de l'objet optional, même si aucune valeur n'est présente. Cela entraîne des constructions et destructions inutiles...
Implémentation
Utilisation d'une union pour le stockage
Puisque la construction et la destruction se produisent toujours, existe-t-il un moyen de ne pas construire lorsqu'il n'y a pas de valeur ? En C++, les objets de type union ne sont pas automatiquement construits ni détruits. Lorsqu'une union contient plusieurs membres, il est impossible pour celle-ci de savoir quel membre est actif, donc elle ne peut pas effectuer automatiquement les opérations de construction/détruction.
union{
T value;
};
Cette approche permet de contrôler précisément les moments de construction et de destruction.
Construction avec valeur
optional(T &&value) noexcept : _has_value(true) {
new (&_value) T(std::move(value));
}
optional(T const &value) noexcept : _has_value(true) {
new (&_value) T(value);
}
Nous proposons deux méthodes de construction à partir d'une valeur, respectivement pour les rvalues et les lvalues.
L'aspect important réside dans la construction de la valeur. Une union alloue par défaut la taille maximale nécessaire pour ses membres. Puisque notre union ne contient qu'un seul membre value, la taille allouée correspond à celle de T. Nous utilisons alors un placement new pour construire directement T dans la mémoire pré-allouée :
new (&value) T(value);
Cela construit le membre value de l'union à partir de la valeur passée comme argument.
Construction sans valeur
Pour prendre en charge la construction d'une valeur vide, nous créons un type auxiliaire représentant l'absence de valeur.
struct nullopt_t {
explicit nullopt_t() = default;
};
inline constexpr nullopt_t nullopt;
Nous définissons ici le type nullopt_t et instançions un objet global nullopt. La construction d'un optional vide se fait en passant ce type auxiliaire.
optional(nullopt_t) noexcept : _has_value(false) {}
optional() noexcept : optional(nullopt) {}
Copie et déplacement
Les constructeurs de copie et de déplacement, ainsi que leurs opérateurs d'affectation, suivent des schémas similaires.
// Constructeurs de copie/déplacement
optional(optional const &other) : _has_value(other._has_value) {
if (_has_value) {
new (&_value) T(other._value);
}
}
optional(optional &&other) noexcept : _has_value(other._has_value) {
if (_has_value) {
new (&_value) T(std::move(other._value));
}
}
// Affectation à une valeur vide
optional &operator=(nullopt_t) noexcept {
if (_has_value) {
_value.~T();
_has_value = false;
}
return *this;
}
// Affectation à une valeur
optional &operator=(T &&value) noexcept {
if (_has_value) {
this->_value = std::move(value); // Attention : problème ici !
} else {
new (&this->_value) T(std::move(value));
_has_value = true;
}
return *this;
}
// Affectation par copie
optional &operator=(optional const &other) {
if (this == &other) {
return *this;
}
if (_has_value) {
_value.~T();
_has_value = false;
}
if (other._has_value) {
new (&_value) T(other._value);
_has_value = true;
}
return *this;
}
// Affectation par déplacement
optional &operator=(optional &&other) noexcept {
if (this == &other) {
return *this;
}
if (_has_value && other._has_value) {
_value = std::move(other._value); // Attention : devrait être other._value
_has_value = true;
} else if (_has_value) {
_value.~T();
_has_value = false;
} else if (other._has_value) {
new (&_value) T(std::move(other._value));
_has_value = true;
}
other._has_value = false; // L'objet source devient vide après le déplacement
return *this;
}
Fonctions emplace
Les fonctions emplace fonctionnent comme celles des conteneurs standards, permettant de construire directement les arguments à l'intérieur plutôt que de les construire avant de les passer. Cela évite un déplacement supplémentaire.
template <class... Args>
void emplace(Args &&...args) {
if (_has_value) {
_value.~T();
_has_value = false;
}
new (&_value) T(std::forward<Args>(args)...);
_has_value = true;
}
template <class U, class... Args>
void emplace(std::initializer_list<U> list, Args &&...args) {
if (_has_value) {
_value.~T();
_has_value = false;
}
new (&_value) T(list, std::forward<Args>(args)...);
_has_value = true;
}
Réinitialisation et destructeur
Détruit la valeur et rend l'optionnel vide.
void reset() noexcept {
if (_has_value) {
_value.~T();
_has_value = false;
}
}
~optional() {
if (_has_value) {
_value.~T();
}
}
Vérification de la valeur
bool has_value() const noexcept {
return _has_value;
}
explicit operator bool() noexcept {
return _has_value;
}
Vérifie si une valeur est présente.
Accès à la valeur avec vérification
L'accès à la valeur nécessite une vérification de sa présence. Si elle n'existe pas, une expection est levée.
Nous définissons ici une exception personnalisée :
struct BadOptionalAccess : std::exception {
BadOptionalAccess() = default;
virtual ~BadOptionalAccess() = default;
char const *what() const noexcept override {
return "BadOptionalAccess";
}
};
Implémentation de la méthode value() :
T const &value() const & {
if (!_has_value) {
throw BadOptionalAccess();
}
return _value;
}
T &value() & {
if (!_has_value) {
throw BadOptionalAccess();
}
return _value;
}
T const &&value() const && {
if (!_has_value) {
throw BadOptionalAccess();
}
return std::move(_value);
}
T &&value() && {
if (!_has_value) {
throw BadOptionalAccess();
}
return std::move(_value);
}
Opérateurs * et -> (accès non vérifié)
T const &operator*() const & noexcept {
return _value;
}
T &operator*() & noexcept {
return _value;
}
T const &&operator*() const && noexcept {
return std::move(_value);
}
T &&operator*() && noexcept {
return std::move(_value);
}
T const *operator->() const noexcept {
return &_value;
}
T *operator->() noexcept {
return &_value;
}
value_or - Valeur par défaut
T value_or(T &&default_value) const & {
return _has_value ? _value : std::move(default_value);
}
T value_or(T const &default_value) const & {
return _has_value ? _value : default_value;
}
Opérateurs de comparaison
bool operator==(optional const &other) const noexcept {
if (_has_value != other._has_value) {
return false;
}
if (_has_value) {
return _value == other._value;
}
return true;
}
bool operator!=(optional const &other) const noexcept {
if (_has_value != other._has_value) {
return true;
}
if (_has_value) {
return _value != other._value;
}
return false;
}
bool operator>(optional const &other) const noexcept {
if (!_has_value || !other._has_value) {
return false;
}
return _value > other._value;
}
bool operator<(optional const &other) const noexcept {
if (!_has_value || !other._has_value) {
return false;
}
return _value < other._value;
}
bool operator>=(optional const &other) const noexcept {
if (!_has_value || !other._has_value) {
return true;
}
return _value >= other._value;
}
bool operator<=(optional const &other) const noexcept {
if (!_has_value || !other._has_value) {
return true;
}
return _value <= other._value;
}
bool operator==(nullopt_t) noexcept {
return !_has_value;
}
friend bool operator==(nullopt_t, optional const &self) noexcept {
return !self._has_value;
}
Opération swap
Utilise using std::swap. Par défaut, cela utilise la fonction de swap standard. Si une fonction swap personnalisée est définie dans l'espace de nommage de other, elle sera utilisée à la place.
void swap(optional &other) noexcept {
if (_has_value & other._has_value) {
using std::swap;
swap(_value, other._value); // Note : devrait être swap(_value, other._value)
} else if (!_has_value & !other._value) {
// Les deux sont vides, rien à faire
} else if (_has_value) {
other.emplace(_value);
reset();
} else {
emplace(std::move(other._value));
other.reset();
}
}
make_optional
Implémentation de make_optional ressemblant à make_shared pour les pointeurs intelligents. Elle permet de construire un optional à partir d'arguments.
Puisque le type de valeur fourni peut inclure des références ou des qualificateurs const, nous utilisons std::decay_t pour obtenir le type réel de la valeur.
#if __cpp_deduction_guides
template <class T>
optional(T) -> optional<T>; // CTAD C++17
#endif
template <typename T>
optional<std::decay_t<T>> make_optional(T &&value) {
return optional<std::decay_t<T>>(std::forward<T>(value));
}
template <typename T, typename... Args>
optional<T> make_optional(Args &&...args) {
return optional<T>(std::forward<Args>(args)...);
}
Construction en place
Par exemple, si un conteneur stocke une structure :
struct ss {
int a;
int b;
ss(int a, int b) : a(a), b(b) {}
};
optional<ss> o4
Pour construire o4 directement avec des paramètres, nous utilisons un type auxiliaire inplace_t et un objet global inplace.
Le constructeur prend comme premier paramètre un objet de type inplace_t, permettant de transmettre les arguments directement à la fonction de construction interne :
struct inplace_t {
explicit inplace_t() = default;
};
inline constexpr inplace_t inplace;
template <typename... Args>
explicit optional(inplace_t, Args &&...args) : _has_value(true) {
new (&_value) T(std::forward<Args>(args)...);
}
template <typename U, class... Args>
explicit optional(inplace_t, std::initializer_list<U> list, Args &&...args)
: _has_value(true) {
new (&_value) T(list, std::forward<Args>(args)...);
}
Code de test
#include "optional.hpp"
#include <iostream>
#include <vector>
auto functory(int a) -> optional<int> {
if (a > 10) {
return optional<int>(a);
} else {
return nullopt;
}
}
struct ss {
int a;
int b;
ss(int a, int b) : a(a), b(b) {}
};
int main(int, char **) {
optional<std::vector<int>> o1({1, 2, 3, 4, 54, 6, 7, 8});
if (o1) {
for (auto &p: o1.value()) {
std::cout << p << " ";
}
}
std::cout << std::endl;
auto o2 = make_optional<std::vector<int>>({2, 5, 7, 34, 3, 7, 89, 4, 2});
if (o2) {
for (auto &p: o2.value()) {
std::cout << p << " ";
}
}
std::cout << std::endl;
optional<std::vector<int>> o3({1, 2, 3, 4, 5, 6, 7, 8, 9});
if (o3) {
for (auto &p: o3.value()) {
std::cout << p << " ";
}
}
optional<ss> o4(inplace, 2, 3);
long long int a;
while (std::cin >> a) {
auto p = functory(a);
if (p || p.has_value()) {
std::cout << "*p:" << *p << std::endl;
std::cout << "p.value():" << p.value() << std::endl;
} else {
std::cout << "p.has_value:" << p.has_value() << std::endl;
std::cout << "p.valur_or(65):" << p.value_or(65) << std::endl;
}
}
}
Différentes façons de construire et tests simples des méthodes sont effectués, répondant à nos besoins.
Code source
#pragma once
#include <exception>
#include <initializer_list>
#include <type_traits>
struct BadOptionalAccess : std::exception {
BadOptionalAccess() = default;
virtual ~BadOptionalAccess() = default;
char const *what() const noexcept override {
return "BadOptionalAccess";
}
};
struct nullopt_t {
explicit nullopt_t() = default;
};
inline constexpr nullopt_t nullopt;
struct inplace_t {
explicit inplace_t() = default;
};
inline constexpr inplace_t inplace;
template <typename T>
struct optional {
private:
bool _has_value;
union {
T _value;
};
public:
optional(T &&value) noexcept : _has_value(true) {
new (&_value) T(std::move(value));
}
optional(T const &value) noexcept : _has_value(true) {
new (&_value) T(value);
}
optional() noexcept : optional(nullopt) {}
optional(nullopt_t) noexcept : _has_value(false) {}
template <typename... Args>
explicit optional(inplace_t, Args &&...args) : _has_value(true) {
new (&_value) T(std::forward<Args>(args)...);
}
template <typename U, class... Args>
explicit optional(inplace_t, std::initializer_list<U> list, Args &&...args)
: _has_value(true) {
new (&_value) T(list, std::forward<Args>(args)...);
}
optional(optional const &other) : _has_value(other._has_value) {
if (_has_value) {
new (&_value) T(other._value);
}
}
optional(optional &&other) noexcept : _has_value(other._has_value) {
if (_has_value) {
new (&_value) T(std::move(other._value));
}
}
optional &operator=(nullopt_t) noexcept {
if (_has_value) {
_value.~T();
_has_value = false;
}
return *this;
}
optional &operator=(T &&value) noexcept {
if (_has_value) {
_value = std::move(value);
} else {
new (&_value) T(std::move(value));
_has_value = true;
}
return *this;
}
optional &operator=(T const &_value) noexcept {
if (_has_value) {
_value.~T();
_has_value = false;
}
new (&_value) T(_value);
_has_value = true;
return *this;
}
optional &operator=(optional const &other) {
if (this == &other) {
return *this;
}
if (_has_value) {
_value.~T();
_has_value = false;
}
if (other._has_value) {
new (&_value) T(other._value);
}
_has_value = other._has_value;
return *this;
}
optional &operator=(optional &&other) noexcept {
if (this == &other) {
return *this;
}
if (_has_value & other._has_value) {
_value = std::move(other._has_value);
_has_value = true;
} else if (_has_value) {
_value.~T();
_has_value = false;
} else if (other._has_value) {
new (&_value) T(std::move(other._value));
_has_value = true;
}
other._has_value = false;
return *this;
}
template <class... Args>
void emplace(Args &&...args) {
if (_has_value) {
_value.~T();
_has_value = false;
}
new (&_value) T(std::forward<Args>(args)...);
_has_value = true;
}
template <class U, class... Args>
void emplace(std::initializer_list<U> list, Args &&...args) {
if (_has_value) {
_value.~T();
_has_value = false;
}
new (&_value) T(list, std::forward<Args>(args)...);
_has_value = true;
}
void reset() noexcept {
if (_has_value) {
_value.~T();
_has_value = false;
}
}
~optional() {
if (_has_value) {
_value.~T();
}
}
bool has_value() const noexcept {
return _has_value;
}
explicit operator bool() noexcept {
return _has_value;
}
bool operator==(nullopt_t) noexcept {
return !_has_value;
}
friend bool operator==(nullopt_t, optional const &self) noexcept {
return !self._has_value;
}
bool operator==(optional const &other) noexcept {
if (_has_value != other._has_value) {
return false;
}
return _value == other._value;
}
bool operator!=(nullopt_t) const noexcept {
return _has_value;
}
friend bool operator!=(nullopt_t, optional const &self) noexcept {
return self._has_value;
}
bool operator!=(optional const &other) noexcept {
if (_has_value == other._has_value) {
return false;
}
return _value != other._value;
}
T const &value() const & {
if (!_has_value) {
throw BadOptionalAccess();
}
return _value;
}
T &value() & {
if (!_has_value) {
throw BadOptionalAccess();
}
return _value;
}
T const &&value() const && {
if (!_has_value) {
throw BadOptionalAccess();
}
return std::move(_value);
}
T &&value() && {
if (!_has_value) {
throw BadOptionalAccess();
}
return std::move(_value);
}
T const &operator*() const & noexcept {
return _value;
}
T &operator*() & noexcept {
return _value;
}
T const &&operator*() const && noexcept {
return std::move(_value);
}
T &&operator*() && noexcept {
return std::move(_value);
}
T const *operator->() const noexcept {
return &_value;
}
T *operator->() noexcept {
return &_value;
}
T value_or(T &&default_value) const & {
return _has_value ? _value : std::move(default_value);
}
T value_or(T const &default_value) const & {
return _has_value ? _value : default_value;
}
bool operator==(optional<T> const &other) const noexcept {
if (_has_value != other._has_value) {
return false;
}
if (_has_value) {
return _value == other._value;
}
return true;
}
bool operator!=(optional const &other) const noexcept {
if (_has_value != other._has_value) {
return true;
}
if (_has_value) {
return _value != other._value;
}
return false;
}
bool operator>(optional const &other) const noexcept {
if (!_has_value || !other._has_value) {
return false;
}
return _value > other._value;
}
bool operator<(optional const &other) const noexcept {
if (!_has_value || !other._has_value) {
return false;
}
return _value < other._value;
}
bool operator>=(optional const &other) const noexcept {
if (!_has_value || !other._has_value) {
return true;
}
return _value >= other._value;
}
bool operator<=(optional const &other) const noexcept {
if (!_has_value || !other._has_value) {
return true;
}
return _value <= other._value;
}
void swap(optional &other) noexcept {
if (_has_value & other._has_value) {
using std::swap;
swap(_value, other._has_value);
} else if (!_has_value & !other._has_value) {
// do nothing
} else if (_has_value) {
other.emplace(_value);
reset();
} else {
emplace(std::move(other._value));
other.reset();
}
}
};
#if __cpp_deduction_guides
template <class T> // C++17 seulement
optional(T) -> optional<T>;
#endif
template <typename T>
optional<std::decay_t<T>> make_optional(T &&value) {
return optional<std::decay_t<T>>(std::forward<T>(value));
}
template <typename T, typename... Args>
optional<T> make_optional(Args &&...args) {
return optional<T>(std::forward<Args>(args)...);
}