IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Apprendre la construction de classes en C++ avec les types et les objets de Stepanov,
Un tutoriel de KDAB

Le , par Community Management

10PARTAGES

10  0 
Chers membres du club,

J'ai le plaisir de vous présenter ce tutoriel de KDAB pour apprendre la construction de classes en C++ avec les types réguliers et les objets partiellement formés de Stepanov.


Dans ce tutoriel, j'aborderai un des concepts fondamentaux introduits par Alex Stepanov et Paul McJones dans leur ouvrage de référence Elements of Programming : celui de type régulier (ou semi-régulier) (regular type) et d'état partiellement formé (partially-formed state).
À partir de ces concepts, j'essaierai d'en déduire des règles d'implémentation en C++ de ce que l'on appelle d'habitude des « types valeur » (value types), en me concentrant sur l'essentiel, qui me semble n'avoir pas été traité suffisamment en profondeur jusqu'à présent : les fonctions membres spéciales.

Bonne lecture et n'hésitez pas à apporter vos commentaires.

Retrouvez les autres cours et tutoriels proposés par KDAB

Retrouvez les meilleurs cours et tutoriels pour apprendre C++

Une erreur dans cette actualité ? Signalez-nous-la !

Avatar de Luc Hermitte
Expert éminent sénior https://www.developpez.com
Le 14/04/2017 à 19:27
(Je reposte ici un commentaire posé dans la zone de rédaction)

La discussion de l'auteur sur le partiellement formé n'est pas anodine. C'est un sujet qui a fortement impacté le redesign des variants, d'optional et d'autres trucs encore comparativement au design initial chez boost. Je pense que c'est un sujet sur lequel nous nous cherchons encore.

Pour l'instant, mettre l'objet dans un état destructible (voire affectable), mais non valide (UB en cas de tentative d'exécution de certaines opérations) ne me parait absolument pas aberrant. On rejoint des problématiques de Programmation par Contrat VS programmation Défensive (quand on considère l'utilisation d'exceptions). C'est exactement la même problématique que " auto p = make_unique<T>(...); f(move(p)); p->g();". Il y a une UB sans que cela ne choque personne, non?

Jusqu'au C++11, j'en étais arrivé à la conclusion que les constructeurs par défaut ne sont pas nos amis sur les types entités. Que sur les valeurs pourquoi pas, mais que s'il l'on peut éviter c'est aussi bien. Si maintenant on considère que l'on peut déplacer des valeurs, il y a ce problème d'état valide (qui nous offre toujours la garantie basique (de destructabilité)) mais non exploitable.
2  0 
Avatar de koala01
Expert éminent sénior https://www.developpez.com
Le 08/08/2017 à 16:06
Salut,

Personnellement, mon problème avec les objets partiellement formé serait sans doute double:

D'un coté, je suis ancré à l'idée qu'un constructeur doit permettre d'obtenir un objet utilisable comme un mollusque à la coque d'un navire. Si un objet n'est que partiellement formé (et donc partiellement (in)utilisable) au moment où l'on y a accès, c'est "pas bon", mais, alors là, "pas bon du tout".

Le truc, c'est que je serais bien en peine de justifier ce point de vue, peut-être parce que c'est que que l'on m'a toujours appris, peut-être parce qu'il est plus instinctif qu'autre chose.

D'un autre coté, je peux aussi concevoir le fait que l'on puisse vouloir "subdiviser" la formation d'un élément complexe en plusieurs étapes, ne serait-ce que parce qu'il se peut que l'on ne dispose pas forcément de toutes les informations nécessaires à la formation totale de cet élément complexe.

Le truc, c'est qu'un autre point de vue auquel je suis viscéralement accroché est que "l'utilisateur est un imbécile distrait", qui saisira toutes les opportunités de faire une connerie que l'on pourrait lui laisser, et que -- au risque d'en choquer certains -- les développeurs ne font pas exception à la règle.

Si bien que je ne peux m'empêcher de penser que le gros problème posé par le fait d'avoir un élément partiellement formé concerne le choix qui devra être fait quant à l'accessibilité de cet objet, dans l'état son état actuel, car, pour pouvoir terminer la formation de l'objet, il faut être en mesure de manipuler cet objet.

Mais, d'un autre coté, si on laisse au développeur la possibilité de faire la moindre connerie en manipulant cet objet partiellement formé, il me semble évident qu'il ... la fera tôt ou tard (sans doute plus tôt que tard, d'ailleurs), mais, très certainement, toujours "au pire moment qui soit".

Le gros problème à résoudre est donc qu'il faut laisser "suffisamment de latitude" à l'utilisateur afin de lui permettre de terminer la formation de l'élément tout en l'empêchant systématiquement de manipuler cet objet d'une manière qui mènerait à la catastrophe (conséquence logique d'un comportement indéfini).

S'il y a moyen de concilier "la chèvre et le choux" à ce point de vue -- entre autres au travers des techniques propres à la programmation par contrat -- je ne vois aucune objection majeure à la mise en place d'objets partiellement formé. Mais n'est-ce pas un peu utopique que d'espérer concilier deux aspects si fondamentalement différents
2  0 
Avatar de Pyramidev
Expert éminent https://www.developpez.com
Le 11/04/2017 à 22:37
Personnellement, je suis d'accord avec le premier commentaire de l'article original quand il dit :
Regarding default-initialization for if/else vs ?: — I think that’s not a very strong argument. If it was the sole argument for a partially-formed state, then the safety concerns (use after lack of proper initialization) would far outweigh this minor benefit IMHO. However, it is not the sole argument. It is far easier to perform aggregation if you have a partially constructed state, since the class might not have a default value to provide to its data member.
En particulier, quand on manipule std::array<T, N>, il est très pratique que T ait un constructeur par défaut sans le coût de l'initialisation. Exemple :
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <array>

class Rect {
public:
	Rect() noexcept = default;
	constexpr Rect(int x1, int y1, int x2, int y2) noexcept :
		m_x1(x1), m_y1(y1), m_x2(x2), m_y2(y2) {}
private:
	int m_x1, m_y1, m_x2, m_y2;
};

template<size_t N>
std::array<Rect, N> foo()
{
	std::array<Rect, N> result;
	for(size_t k = 0; k < N; ++k)
		result[k] = Rect(k, k, k+1, k+1);
	return result;
}
Mais l'absence d'initialisation est dangereuse car source de bogues non reproductibles. Pour pallier cela, on pourrait envisager que l'absence d'initialisation soit explicite :
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
struct no_init_t {} no_init;

class Rect2 {
public:
	Rect2(no_init_t) noexcept : Rect2() {}
	constexpr Rect2(int x1, int y1, int x2, int y2) noexcept :
		m_x1(x1), m_y1(y1), m_x2(x2), m_y2(y2) {}
private:
	Rect2() noexcept = default;
	int m_x1, m_y1, m_x2, m_y2;
};
Cependant, d'un autre côté, l'utilisation serait parfois plus complexe. Par exemple, réécrire la fonction foo nécessiterait du code préliminaire :
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace detail
{
	template<class T, size_t...iseq>
	std::array<T, sizeof...(iseq)> make_std_array_no_init_impl(std::index_sequence<iseq...>)
	{
		return {{(static_cast<void>(iseq), T(no_init))...}};
	}
}

//! Pour chaque élément, appelle le constructeur avec en argument no_init.
template<class T, size_t N>
std::array<T, N> make_std_array_no_init()
{
	return detail::make_std_array_no_init_impl<T>(std::make_index_sequence<N>());
}

template<size_t N>
std::array<Rect2, N> foo2()
{
	auto result = make_std_array_no_init<Rect2, N>();
	for(size_t k = 0; k < N; ++k)
		result[k] = Rect2(k, k, k+1, k+1);
	return result;
}
1  0 
Avatar de Luc Hermitte
Expert éminent sénior https://www.developpez.com
Le 08/08/2017 à 16:22
La construction via visiteur, j'ai l'impression que c'est souvent : on a un blog de données duquel on extrait des informations. C'est un filtre sur un blob, ou un nouveau blob filtré.

L'invariant d'exploitabilité ne peut pas être positionné tant que l'on n'a pas fini de visiter.
D'autres invariants ? Pas sûr qu'il y en ait.

Ce qui me fait penser que j'ai tendance à percevoir les types de objets de la sorte maintenant:
- agrégat de données, par vraiment d'invariant, nul besoin de "private" ni autre capsule de protection
- truc qui a un invariant
- valeur
- entité
- autres machines hybrides comme les exceptions
1  0 
Avatar de Pyramidev
Expert éminent https://www.developpez.com
Le 06/08/2017 à 2:24
Petit up.

Rappel sur l'objet du fil : L'article parle d'une manière de concevoir des classes de telle sorte que le constructeur par défaut génère un objet dans un « état partiellement formé », c'est-à-dire un état dans lequel les seules fonctions qui ont un comportement déterminé sont le destructeur et l'affectation.

Je suis récemment tombé sur un autre exemple où un état partiellement formé aurait été utile : construire un objet à partir d'un visiteur.

Soit le code suivant :
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BuildBarFromFooAndConfig final : private FooConstVisitor
{
public:
	BuildBarFromFooAndConfig(const FooBase& foo, const Config& config) :
		m_config{config}, m_builtResult{}
	{
		foo.accept(*this);
	}
	const Bar& getResult() const {return m_builtResult;}
private:
	void visit(const FooDeriv1& visited) override {m_builtResult = /* code qui dépend de visited et de m_config */;}
	void visit(const FooDeriv2& visited) override {m_builtResult = /* code qui dépend de visited et de m_config */;}
	void visit(const FooDeriv3& visited) override {m_builtResult = /* code qui dépend de visited et de m_config */;}
	const Config& m_config;
	Bar           m_builtResult;
};
Dans ce code, m_builtResult est d'abord construit par défaut puis est assigné à la bonne valeur. On ne peut pas le construire directement avec la bonne valeur.
Mais, si Bar n'a pas de constructeur par défaut, on est contraint de changer le code, par exemple comme ceci :
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class BuildBarFromFooAndConfig final : private FooConstVisitor
{
public:
	BuildBarFromFooAndConfig(const FooBase& foo, const Config& config) :
		m_config{config}, m_builtResult{}
	{
		foo.accept(*this);
		assert(m_builtResult);
	}
	const Bar& getResult() const {return *m_builtResult;}
private:
	void visit(const FooDeriv1& visited) override {m_builtResult = /* code qui dépend de visited et de m_config */;}
	void visit(const FooDeriv2& visited) override {m_builtResult = /* code qui dépend de visited et de m_config */;}
	void visit(const FooDeriv3& visited) override {m_builtResult = /* code qui dépend de visited et de m_config */;}
	const Config& m_config;
	//! \remark std::optional because the result is built lately in the constructor.
	std::optional<Bar> m_builtResult;
};
On se trimballe un invariant de classe qui dit que l'objet optionnel m_builtResult n'est pas vraiment optionnel, du moins pas depuis que le corps du constructeur s'est terminé.
0  0