C++20 : Concepts en pratique

Magazine
Marque
GNU/Linux Magazine
HS n°
Numéro
122
Mois de parution
décembre 2022
Spécialité(s)


Résumé

Techniques bien concrètes plutôt que notions philosophiques pour améliorer considérablement l’expressivité de la programmation générique en C++, les concepts introduits par le standard C++20 ouvrent la voie à des codes plus clairs, plus facilement corrigibles en cas d’erreur. Cerise sur le gâteau, les nouvelles syntaxes ne sont pas aussi obscures qu’on aurait pu le craindre.


Body

L’objet des concepts en C++ est de résoudre certains problèmes liés aux templates, tels que décrits par Bjarne Stroustrup lui-même par exemple dans [1]. L’idée remonte au début des années 1990, par Alex Stepanov, le grand monsieur à l’origine de la STL – excusez du peu – et s’appuie sur des travaux plus anciens encore. Pourtant, il faudra attendre l’année 2020 pour que cette idée prenne enfin place dans le standard C++, dans une version dite allégée (light), après plusieurs tentatives avortées sur presque deux décennies.

Mais enfin, les concepts sont là, amenant avec eux une évolution au moins aussi importante que celle engendrée par le standard C++11, l’avènement de ce que l’on nomme depuis le « C++ moderne ». Avec maintenant un peu de recul, il est temps de gratter la surface de toutes ces nouvelles notions en les mettant en pratique sur quelques cas concrets. Si vous voulez approfondir la question dans les recoins les plus obscurs du cœur du langage, la longue page [4] devrait vous combler.

Tout ce qui suit nécessite évidemment de paramétrer le compilateur pour qu’il applique le standard C++20. Pour GCC et Clang, il suffit normalement de passer le paramètre -std=c++20 ou -std=gnu++20. Si vous utilisez CMake, alors il faut définir soit une configuration générale :

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED On)
set(CMAKE_CXX_EXTENSIONS Off)

Soit spécifier pour chaque cible :

set_property(TARGET ma_cible PROPERTY CXX_STANDARD 20)
set_property(TARGET ma_cible PROPERTY CXX_EXTENSIONS Off)
target_compile_features(ma_cible PUBLIC cxx_std_20)

Les déclarations précédentes désactivent les extensions au langage spécifiques du compilateur – c’est la différence entre c++20 et gnu++20 en valeur donnée à -std=. Ce n’est pas strictement nécessaire, mais cela aide à assurer la meilleure portabilité.

1. Ça marche… ou pas

Imaginons une fonction générique dans un grand projet (des millions de lignes de code) prenant trois paramètres et qui retourne la division des deux premiers, augmentée du troisième. Quelque chose comme ça :

template<typename T>
T div_add(T v1, T v2, T v3)
{
   return v1/v2 + v3;
}

Cette fonction, manifestement à vocation de calcul numérique, peut être invoquée en lui passant des floats, des doubles, voire des std::complex, des quaternions, même des polynômes… Jusqu’au jour où le petit nouveau dans le projet décide pour des raisons qui lui sont propres de l’invoquer avec des chaînes de caractères :

// beaucoup de #include ...
#include <string>
using namespace std::string_literals;
auto res = div_add("World"s, "hel"s, "lo"s);

Cela n’a évidemment aucun sens et nul doute que le compilateur va abreuver notre infortuné apprenti d’une flopée d’insultes… sauf que.

Sauf qu’il y a bien longtemps, dans une portion de code très éloignée pré-std::filesystem::path et même pré-Boost, le choix avait été fait d’utiliser std::string pour représenter et manipuler les chemins de fichiers. Et pour simplifier lesdites manipulations, quelqu’un avait eu la bonne idée de surdéfinir operator/() :

string operator/(string const& s1, string const& s2)
{
   return s1 + "/" + s2;
}

Déclaration et définition inline dans un en-tête « bas niveau » inclus par la Terre entière, qui donc se retrouve subrepticement visible dans le code tout neuf du petit nouveau… et qui suffit à rendre l’invocation de div_add<>() avec des chaînes de caractères syntaxiquement valides. Évidemment, les problèmes vont se manifester sans tarder, mais pas à l’endroit de l’instanciation erronée, ce qui complique sensiblement la résolution… et c’est le drame, pleurs et désespoir.

Après tout, la recommandation C.160 [2] n’est pas là par hasard.

Pour éviter de telles mésaventures, qui peuvent arriver plus vite qu’on ne croit dans des codes anciens et de grande taille, il faut « contraindre » d’une façon ou d’une autre la déclaration de div_add<>(). « Avant », on aurait utilisé des choses plus ou moins horribles comme std::enable_if<>, ou au mieux static_assert(), mais ça, c’était avant.

Il est désormais possible de requérir du type effectivement utilisé pour l’instanciation qu’il respecte un prédicat – mais en vérité, ce n’est pas qu’une requête, c’est une contrainte dure. Cela se présente sous cette forme :

template<typename T>
requires std::is_floating_point_v<T>
T div_add(T v1, T v2, T v3)
{ return v1/v2 + v3; }

La clause introduite par requires (qui est un nouveau mot-clef et réservé, en passant) impose une contrainte sur le type qui sera donné en paramètre template. Ici, on utilise un trait issu de l’en-tête <type_traits> permettant de savoir si un type est un nombre en virgule flottante. Dès lors, toute tentative d’instancier la fonction avec un type pour lequel ce trait est faux échouera. Par exemple, en passant des entiers, GCC nous donne le message (un peu abrégé ici) :

error: no matching function for call to ‘div_add(int, int, int)’
note: candidate: ‘template<class T> requires is_floating_point_v<T> T div_add(T, T, T)’
note: template argument deduction/substitution failed:
note: constraints not satisfied

Tandis que Clang nous donne (un peu moins abrégé) :

error: no matching function for call to 'div_add'
note: candidate template ignored: constraints not satisfied [with T = int]
note: because 'std::is_floating_point_v<int>' evaluated to false

Voyez comme les messages d’erreur sont très explicites et décrivent exactement la cause de l’erreur. Il est ainsi facile de repérer ce qui cloche et d’y remédier.

La clause requires est ici placée juste avant le prototype de la fonction, mais elle peut être également placée juste après – c’est essentiellement une affaire de préférence. Elle peut également contenir un prédicat plus élaboré, par exemple (en plaçant la clause après le prototype) :

template<typename T>
T div_add(T v1, T v2, T v3)
requires std::is_floating_point_v<T>
   or std::is_same_v<T, std::complex<double>>
   or std::is_same_v<T, std::complex<float>>
{ return v1/v2 + v3; }

Les traditionalistes remplaceront or par ||. Lorsque le prédicat devient un peu trop sophistiqué, il est vivement recommandé d’utiliser un…

2. Concept nommé

La contrainte précédente était anonyme : simplement donnée là où elle doit s’appliquer. Le problème est qu’elle devrait être répétée partout où elle serait utile… ce qui pourrait être acceptable tant qu’elle est triviale, mais nous connaissons tous les terribles dangers induits par la duplication de code à coups de copier-coller.

L’idée est ici de donner un nom à notre contrainte (ou ensemble de contraintes) pour pouvoir la réutiliser par la suite, en lieu et place de typename (ou class) dans les déclarations template. Ce faisant, on impose une contrainte sémantique au type acceptable par l’entité générique, fonction ou classe : on déclare une nouvelle catégorie de types, on crée un concept.

La syntaxe pour le cas simple précédent est la suivante :

template<typename T>
concept DivAddOk = std::is_floating_point_v<T>;

Cela ressemble un peu à une déclaration using ou à celle d’une class, sauf qu’on utilise l’autre nouveau mot-clef et réservé concept : ce n’est ni plus ni moins que la déclaration d’un nouveau concept, identifié dans notre exemple par le nom évocateur DivAddOk. Pour l’utiliser, deux possibilités. Soit en remplacement du contenu du requires précédent, avant ou après le prototype de la fonction :

template<typename T>
requires DivAddOk<T>
T div_add(T v1, T v2, T v3);

Soit en prenant carrément la place du typename dans la clause template :

template<DivAddOk T>
T div_add(T v1, T v2, T v3);

De par sa concision et sa clarté cristalline, cette syntaxe est sans doute celle qui a la préférence : on voit immédiatement quelle est la sémantique attendue sur les types à utiliser lors des instanciations. Ce qui est remarquable est la disparition des chevrons <> après le nom du concept. En fait, cette écriture condensée pourrait avoir un équivalent qui ressemblerait à ça :

template<typename T requires DivAddOk<T>>

Mais une telle écriture n’est pas autorisée. Tout se passe comme si le paramètre T était implicitement passé en premier paramètre du concept, un peu comme this est un premier paramètre implicite d’une fonction membre d’une classe. Nous verrons plus loin d’autres exemples de cette « implicitisation » du premier paramètre.

De nombreux concepts prédéfinis sont disponibles dans l’en-tête <concepts> [3] et quelques autres. Ils peuvent être combinés pour en construire de plus élaborés, par exemple :

template<typename T>
concept DefCopy =
   std::default_initializable<T> and
   std::copyable<T>;

Ici, on exige du type qu’il soit possible d’en construire une instance avec valeur par défaut et (le and à la fin de la troisième ligne, ou && si vous préférez) qu’il soit possible d’en copier une valeur dans une autre instance. Des types comme int ou std::vector<float> peuvent convenir, mais pas std::unique_ptr<> (qui n’est pas copiable) ni std::atomic_ref<> (qui n’a pas de constructeur par défaut).

Ou encore :

template<typename T>
concept String =
   std::derived_from<T, std::string> or
   std::assignable_from<T&, char const*>;

Un type acceptable doit soit être un type dérivé de std::string, soit autoriser une affectation à partir d’une chaîne char const* – l’alternative est introduite par le or (ou ||). Petite subtilité, remarquez l’ajout de la référence & au type générique T dans l’utilisation du concept std::assignable_from<> : en effet, pour pouvoir affecter une valeur à « quelque chose », il est nécessaire que le type de ce « quelque chose » soit une référence sur un objet, techniquement une lvalue-reference.

Ce dernier exemple montre que des concepts peuvent impliquer plus d’un élément. Imaginons une fonction statique qui n’opère que sur des tableaux statiques de petites tailles, typiquement des points 2D ou 3D. Quelque chose comme ça :

template<size_t S, typename T>
void pt_func(std::array<T,S> v) { … }

Il serait raisonnable de contraindre le type T à être un nombre en virgule flottante, mais aussi d’interdire les tailles pour lesquelles la fonction n’aurait pas de sens ou ne serait pas prévue. On pourrait écrire cette contrainte ainsi :

template<size_t S, typename T>
requires std::floating_point<T>
   and ((S == 2) or (S == 3))
void pt_func(std::array<T,S> v) { }

Si l’on donne un nom à ce concept, alors on doit utiliser la première écriture présentée au début de cette section. En effet, le concept n’est pas utilisé pour définir une catégorie de types comme dans le cas de DivOk précédent, mais seulement pour assurer une contrainte sur les paramètres template :

template<size_t S, typename T>
concept PtOk = std::floating_point<T>
   and ((S == 2) or (S == 3));
 
template<size_t S, typename T>
requires PtOk<S,T>
void pt_func(std::array<T,S> v) { }

Nous verrons plus loin comment déclarer et utiliser des concepts impliquant réellement plusieurs types en interaction. La déclaration d’un véritable concept de point (ou vecteur) auquel pourraient satisfaire des types aussi variés que le Vector2f de Eigen ou le dvec3 de GLM est laissé en exercice au lecteur (bon courage).

3. Surcharges

Un aspect intéressant est que les concepts participent à la résolution de la surcharge d’une fonction à invoquer. Par exemple :

// 1
template<typename T>
void fct(T t) { … }
 
// 2
template<typename T>
requires std::is_integral_v<T>
void fct(T t) { … }
 
// 3
template<std::floating_point Real>
void fct(Real r) { … }
 
// 4
void fct(std::string const& s) { … }

Nous avons donc :

  1. Une fonction générique tout à fait classique.
  2. Une fonction générique contrainte par une clause requires utilisant une propriété de type pour exiger un type numérique entier (int, long, etc.).
  3. Une fonction générique dont le paramètre de type est contraint par un concept prédéfini pour exiger un type numérique en virgule flottante (float, double, etc.).
  4. Enfin, une fonction « normale » prenant une chaîne en paramètre.

Remarquez que toutes ces fonctions portent le même nom : fct. La magie opère au moment des appels à cette – ou ces – fonction(s) :

f("foo");       // invoque 1
f(list<int>{}); // invoque 1
f(1);           // invoque 2
f(char(1));     // invoque 2
f(1.0f);        // invoque 3
f(1.0);         // invoque 3
f("foo"s);      // invoque 4

Pour le dernier appel, on suppose la présence d’un using namespace std::string_literals : le suffixe ""s va alors effectivement produire une instance de std::string, plutôt qu’un char const* comme c’est le cas dans le premier appel.

Le compilateur va simplement appliquer la règle du « best match », c’est-à-dire trouver la meilleure correspondance avec le type effectif du paramètre. S’il n’y avait pas la déclaration totalement générique, alors le second appel serait une erreur… mais pas le premier. Comme il y a conversion implicite du type du paramètre, char const*, vers std::string, alors la dernière fonction est valable.

Ce qui précède s’applique également aux fonctions membres d’une classe, que la classe elle-même soit générique ou seulement la fonction.

4. Contraindre une API

Imaginons une fonction qui reçoit en paramètre une fonction de rappel (callback), par exemple dans le but de signaler la progression d’un traitement long. En plus de cela, on pourrait attendre de cette callback qu’elle retourne un booléen pour indiquer si l’on doit interrompre le traitement. Imaginez une barre de progression dans une interface graphique flanquée d’un bouton Annuler. De façon très générique, notre fonction pourrait prendre cette forme :

template<typename Callback>
void func(size_t N, Callback cb)
{
   for(size_t i = 0; i < N; ++i)
   {
      // traitement...
      if(cb(i, N))
         break;
   }
}

Déclarée ainsi, on pourrait passer au paramètre cb n’importe quoi qui fournit un operator() prenant les « bons » paramètres et retournant « quelque chose » pouvant être testé dans un cadre booléen : une fonction libre, une lambda, une instance d’une classe plus complexe… Évidemment, si le prototype disponible n’est pas celui attendu, le compilateur se fera un plaisir de nous submerger de messages.

Pour ce cas précis mais néanmoins fort commun, il existe un concept prédéfini : std::invocable<>, déclaré dans l’en-tête <concepts>. On peut l’utiliser ainsi :

#include <concepts>
template<typename Callback>
requires std::invocable<Callback&, size_t, size_t>
void func(size_t N, Callback cb) { … }

Si l’on donne « quelque chose » dans cb dont le prototype ne prend pas deux size_t, le compilateur nous signalera l’erreur à l’endroit de l’appel à func<>(). Sans la clause requires, l’erreur serait signalée dans func<>() et il faudrait retrouver l’origine dans les nombreux messages.

Mais cela ne suffit pas : il faut encore que la callback retourne un booléen, ou au moins un type testable comme un booléen (comme un simple int). Sinon, à nouveau l’erreur sera signalée dans func<>(), non pas à l’endroit de l’appel incorrect. Pour améliorer la contrainte sur le type de retour de la callback, on va définir un nouveau concept pour « enrichir » std::invocable<> :

template<typename CB>
concept Callback = std::invocable<CB&, size_t, size_t>
and requires(CB&& cb, size_t s1, size_t s2)
{
   { cb(s1,s2) } -> std::convertible_to<bool>;
};
 
template<Callback CB>
void func(size_t N, CB cb) { … }

Voyez comment est déclaré le nouveau concept Callback. On commence par réutiliser std::invocable<>, comme précédemment. Puis on ajoute une clause requires, laquelle est en fait une liste de prédicats que l’on doit vérifier pour être conforme au concept. Pour écrire ces prédicats, on se donne des objets à partir desquels on va construire des expressions, lesquelles doivent être syntaxiquement valides. Comme si l’on donnait les paramètres d’une fonction : ici, on se donne cb, une instance du type générique CB, et s1 et s2, deux instances de size_t. Ensuite, l’unique prédicat spécifie à lui seul deux choses :

  • d’une part, l’expression cb(s1,s2) doit être syntaxiquement valide, ce qui était en fait déjà assuré par std::invocable<> ;
  • d’autre part, cette expression doit être une valeur qui peut être implicitement convertie en bool.

On retrouve la syntaxe « moderne » pour spécifier le type de retour d’une fonction avec une flèche, mais remarquez qu’il s’agit en fait d’un concept : c’est imposé par la syntaxe, on ne peut pas donner directement un type comme bool. De plus, en réalité le concept std::convertible_to<> prend deux paramètres : le type « source » à convertir et le type « cible » vers lequel convertir – comme le trait std::is_convertible<>. On ne donne que le second, le premier étant implicitement le type de l’expression dans les accolades. À nouveau, un peu comme this est un premier paramètre implicite des fonctions membres d’une classe.

Nanti de ce nouveau concept, la déclaration de func<>() est non seulement grandement simplifiée, mais en plus toute tentative de donner une callback qui n’est pas conforme aux attentes sera clairement signalée, non pas au sein de func<>() comme ce serait le cas dans la première version classique au début de cette section, mais bien à l’endroit où func<>() est invoquée. Ce qui est bien plus pratique.

La clause requires dans la définition d’un concept peut contenir plusieurs prédicats, ce qui permet de spécifier assez finement l’API attendue du type générique. Par exemple, imaginons que nous voulions en plus un objet de journalisation qui autoriserait une structure hiérarchique. On pourrait spécifier un tel objet ainsi :

template<typename T>
concept LogObj = requires(T& log)
{
   { log.startSection(std::string{}) };
   { log.log(std::string{}) };
   { log.endSection() };
};

Et l’on pourrait l’utiliser ainsi :

template<Callback CB, LogObj LOG>
void func4(size_t N, CB cb, LOG log)
{
   log.startSection("titre");
   for(size_t i = 0; i < N; ++i)
   {
      log.log("étape "s + std::to_string(i));
      if(cb(i, N))
         break;
   }
   log.endSection();
}

À nouveau, l’intérêt d’utiliser un concept par rapport à une fonction générique « classique » est de signaler une incohérence (ici, un non-respect d’une API) au moment de l’instanciation plutôt qu’à l’endroit où l’incohérence se manifeste à l’utilisation.

Pour être complet, le concept Callback tel que déclaré ici est en fait comparable au concept standard std::predicate<>, lequel représente tout objet invocable et dont l’invocation produit une valeur testable dans un contexte booléen.

Le cas des crochets

Exiger d’un type qu’il fournisse un operator[] peut induire une petite subtilité, selon ce que vous voulez accepter. Par exemple, une déclaration « naïve » :

template<typename T>
concept Cpt = requires(T const& t, size_t i) {
   { t[i] } -> std::floating_point;
};

Et une fonction qui l’exploite :

template<Cpt C>
void g(C const& val) { … }

Que se passe-t-il si l’on passe un std::array<float,2>, qui à première vue semble répondre à la contrainte ? On obtient une erreur de compilation, que l’on peut résumer par :

'const float &' does not satisfy 'floating_point'

… car l’operator[] de std::array<> retourne une référence, pas une valeur. Ce qui est bien souvent le cas. Or, le concept std::floating_point<> exige une valeur.

Au moins deux possibilités pour contourner la difficulté. Changer la déclaration du concept pour « retirer » la référence, par exemple :

template<typename T>
concept Cpt = requires(T const& t, size_t i) {
   { std::remove_reference_t<decltype(t[i])>{} }
      -> std::floating_point;
};

Ou en utilisant une clause requires imbriquée sur laquelle nous ne nous étendrons pas ici (attention, comptez bien les chevrons), mais qui permet d’éviter de supposer un constructeur par défaut :

template<typename T>
concept Cpt = requires(T const& t, size_t i) {
   requires std::floating_point<
      std::remove_reference_t<decltype(t[i])> >;
};

Ou carrément redéfinir un nouveau concept sur la base de std::floating_point<>, mais qui accepterait les références, quelque chose comme :

template<typename T>
concept MyFloatingPoint =
   std::floating_point<std::remove_reference_t<T>>;

La seconde solution est préférable, car réutilisable.

5. Prédicats croisés

Considérons quelque chose d’un peu plus sophistiqué : la notion de maillage triangulé pour modéliser une forme dans l’espace – un mesh. Nous aurons donc besoin des notions de point, de triangle indexé et de mesh pour alimenter une fonction générique qui doit effectuer une certaine opération à partir de ces données. Il semble évident que si « point » et « triangle indexé » (triangle défini par trois indices de sommets plutôt que les sommets eux-mêmes) peuvent être complètement séparés, ces notions doivent fonctionner en harmonie avec « mesh ».

Supposons que nous disposions déjà des concepts de point et de triangle :

template<typename T>
concept Point = /* … */;
template<typename T>
concept Triangle = /* … */;

Alors on pourrait déclarer ainsi le concept de mesh, avec une API minimale :

template<typename Mesh_t, typename Point_t, typename Triangle_t>
concept Mesh =
   Point<Point_t> and
   Triangle<Triangle_t>
   and requires(
      Point_t const& p,
      Triangle_t const& t,
      Mesh_t const& m,
      size_t i)
{
   { m.point(i) } -> Point;
   { m.triangle(i) } -> Triangle;
   { m.addPoint(p) } -> std::convertible_to<size_t>;
   { m.addTriangle(t) } -> std::convertible_to<size_t>;
};

Remarquez comme on retrouve std::convertible_to<> déjà vu plus haut pour les fonctions membres add*().

Maintenant, utilisons notre concept. Il y a plusieurs façons de déclarer une fonction contrainte par lui, en voici deux exemples. D’abord avec une clause requires explicite après le prototype :

template<typename P, typename T, typename M>
void func( /* paramètres… */ )
requires Mesh<M,P,T>
{ /* implémentation… */ }

Les contraintes sur les types qui représentent points et triangles sont « cachées » dans la contrainte sur le type destiné à représenter le mesh. Dans cette situation, on donne explicitement tous les paramètres nécessaires au concept, dans le bon ordre.

On peut également spécifier les concepts attendus dans la clause template :

template<Point P, Triangle T, Mesh<P,T> M>
void func( /* paramètres… */ )
{ /* implémentation… */ }

Voyez comme l’utilisation du concept Mesh<> diffère de l’écriture précédente. On ne donne que les derniers paramètres : le premier est implicite. Comme c’est en fait toujours le cas lorsque le concept est utilisé à la place d’un argument typename (ou class) dans une clause template.

Conclusion

Nous en resterons là pour cette présentation des concepts en C++20. Il y aurait encore beaucoup à dire, des syntaxes et formes d’écritures à présenter, notamment les déclarations génériques simplifiées à base de auto. Ou encore les contraintes et clauses requires imbriquées. Mais ce qui précède devrait déjà vous permettre de renforcer sensiblement vos programmes existants. Pour finir sur une dernière petite curiosité, les concepts sont des prédicats au sens premier du terme… c’est-à-dire des booléens utilisables comme expressions constexpr, par exemple :

template<typename T>
void pred(T t) {
   constexpr bool is_int = std::integral<T>;
   /* … */
}

Il est intéressant de voir comment le langage C++ semble évoluer progressivement vers un système de typage plus strict, plus fort, mais en conservant malgré tout une certaine souplesse. Les conversions implicites héritées du C ou explicitement définies sont toujours présentes, mais de plus en plus de moyens sont proposés pour que les relations sémantiques entre les types soient aussi explicites que possible.

Bien que cela soit sûrement pousser le bouchon un peu loin, les concepts que nous venons d’explorer ne sont pas sans rappeler les possibilités des déclarations generic de Ada, langage strict par excellence. Peut-on y voir une forme de lente convergence au fil des décennies ?

Références

[1] Concepts: The Future of Generic Programming, Bjarne Stroustrup : 
https://www.stroustrup.com/good_concepts.pdf

[2] Recommandation C.160 :
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Ro-conventional

[3] Concepts standard : https://en.cppreference.com/w/cpp/concepts

[4] Constraints and concepts : https://en.cppreference.com/w/cpp/language/constraints



Article rédigé par

Par le(s) même(s) auteur(s)

Qt on the Web

Magazine
Marque
GNU/Linux Magazine
Numéro
219
Mois de parution
octobre 2018
Spécialité(s)
Résumé

Le cadriciel C++ Qt est considéré depuis longtemps comme l’un des meilleurs pour le développement d’applications, qu’elles soient dotées ou non d’une interface graphique, qu’elles soient destinées aux PC classiques, aux appareils mobiles ou aux systèmes embarqués. Qt est ainsi disponible pour pratiquement toutes les plateformes majeures existantes. Il en restait une dernière à conquérir : celle des applications web. Ce qui était un vieux rêve pour beaucoup est en passe de devenir une réalité.

Retrouvez vos « bons » vieux jeux DOS avec DOSBox

Magazine
Marque
Linux Pratique
HS n°
Numéro
41
Mois de parution
février 2018
Spécialité(s)
Résumé

Dès les débuts du PC au commencement des années 80 (du siècle dernier !), des jeux sont apparus pour ce qui était alors le système roi, l’horrible DOS – alimentant une guéguerre assez puérile avec les partisans d’Apple, d’Amiga ou des consoles dédiées. Certains jeux connurent un énorme succès, mais beaucoup disparurent avec le DOS il y a presque vingt ans. Heureusement, ces anciennes gloires peuvent ressusciter grâce à l’émulateur DOSBox.

Les derniers articles Premiums

Les derniers articles Premium

Le combo gagnant de la virtualisation : QEMU et KVM

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

C’est un fait : la virtualisation est partout ! Que ce soit pour la flexibilité des systèmes ou bien leur sécurité, l’adoption de la virtualisation augmente dans toutes les organisations depuis des années. Dans cet article, nous allons nous focaliser sur deux technologies : QEMU et KVM. En combinant les deux, il est possible de créer des environnements de virtualisation très robustes.

Brève introduction pratique à ZFS

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Il est grand temps de passer à un système de fichiers plus robuste et performant : ZFS. Avec ses fonctionnalités avancées, il assure une intégrité des données inégalée et simplifie la gestion des volumes de stockage. Il permet aussi de faire des snapshots, des clones, et de la déduplication, il est donc la solution idéale pour les environnements de stockage critiques. Découvrons ensemble pourquoi ZFS est LE choix incontournable pour l'avenir du stockage de données.

Générez votre serveur JEE sur-mesure avec Wildfly Glow

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Et, si, en une ligne de commandes, on pouvait reconstruire son serveur JEE pour qu’il soit configuré, sur mesure, pour les besoins des applications qu’il embarque ? Et si on pouvait aller encore plus loin, en distribuant l’ensemble, assemblé sous la forme d’un jar exécutable ? Et si on pouvait même déployer le tout, automatiquement, sur OpenShift ? Grâce à Wildfly Glow [1], c’est possible ! Tout du moins, pour le serveur JEE open source Wildfly [2]. Démonstration dans cet article.

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 65 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous