web-dev-qa-db-fra.com

Quelles nouvelles fonctionnalités les littéraux définis par l'utilisateur ajoutent-ils au C ++?

C++ 11 introduit littéraux définis par l'utilisateur qui permettra l'introduction d'une nouvelle syntaxe littérale basée sur les littéraux existants (int, hex , string, float) pour que tout type puisse avoir une présentation littérale.

Exemples:

// imaginary numbers
std::complex<long double> operator "" _i(long double d) // cooked form
{ 
    return std::complex<long double>(0, d); 
}
auto val = 3.14_i; // val = complex<long double>(0, 3.14)

// binary values
int operator "" _B(const char*); // raw form
int answer = 101010_B; // answer = 42

// std::string
std::string operator "" _s(const char* str, size_t /*length*/) 
{ 
    return std::string(str); 
}

auto hi = "hello"_s + " world"; // + works, "hello"_s is a string not a pointer

// units
assert(1_kg == 2.2_lb); // give or take 0.00462262 pounds

À première vue, cela semble très cool, mais je me demande à quel point c'est vraiment applicable, quand j'ai essayé de penser à avoir les suffixes _AD et _BC créer des dates J'ai trouvé que c'était problématique en raison de l'ordre de l'opérateur. 1974/01/06_AD évaluerait d'abord 1974/01 (en clair int s) et seulement plus tard, le 06_AD (sans parler des mois d'août et septembre devant être écrits sans le 0 pour des raisons octales). Cela peut être contourné en ayant la syntaxe être 1974-1/6_AD pour que l'ordre d'évaluation de l'opérateur fonctionne, mais il est maladroit.

Donc, ma question se résume à ceci: pensez-vous que cette fonctionnalité se justifiera? Quels autres littéraux aimeriez-vous définir pour rendre votre code C++ plus lisible?


Syntaxe mise à jour pour correspondre à la version finale de juin 2011

137
Motti

Voici un cas où il y a un avantage à utiliser des littéraux définis par l'utilisateur au lieu d'un appel de constructeur:

#include <bitset>
#include <iostream>

template<char... Bits>
  struct checkbits
  {
    static const bool valid = false;
  };

template<char High, char... Bits>
  struct checkbits<High, Bits...>
  {
    static const bool valid = (High == '0' || High == '1')
                   && checkbits<Bits...>::valid;
  };

template<char High>
  struct checkbits<High>
  {
    static const bool valid = (High == '0' || High == '1');
  };

template<char... Bits>
  inline constexpr std::bitset<sizeof...(Bits)>
  operator"" _bits() noexcept
  {
    static_assert(checkbits<Bits...>::valid, "invalid digit in binary string");
    return std::bitset<sizeof...(Bits)>((char []){Bits..., '\0'});
  }

int
main()
{
  auto bits = 0101010101010101010101010101010101010101010101010101010101010101_bits;
  std::cout << bits << std::endl;
  std::cout << "size = " << bits.size() << std::endl;
  std::cout << "count = " << bits.count() << std::endl;
  std::cout << "value = " << bits.to_ullong() << std::endl;

  //  This triggers the static_assert at compile time.
  auto badbits = 2101010101010101010101010101010101010101010101010101010101010101_bits;

  //  This throws at run time.
  std::bitset<64> badbits2("2101010101010101010101010101010101010101010101010101010101010101_bits");
}

L'avantage est qu'une exception au moment de l'exécution est convertie en une erreur au moment de la compilation. Vous ne pouviez pas ajouter l'assertion statique au processeur de bits en prenant une chaîne (du moins pas sans arguments de modèle de chaîne).

69
emsr

À première vue, il semble être un simple sucre syntaxique.

Mais en regardant plus en profondeur, nous voyons que c'est plus que du sucre syntaxique, car il étend les options de l'utilisateur C++ pour créer des types définis par l'utilisateur qui se comportent exactement comme des types intégrés distincts. En cela, ce petit "bonus" est un ajout C++ 11 très intéressant à C++.

En avons-nous vraiment besoin en C++?

Je vois peu d'utilisations dans le code que j'ai écrit ces dernières années, mais ce n'est pas parce que je ne l'ai pas utilisé en C++ que ce n'est pas intéressant pour un autre développeur C++.

Nous avions utilisé en C++ (et en C, je suppose), des littéraux définis par le compilateur, pour taper des nombres entiers en tant qu'entiers courts ou longs, des nombres réels en float ou double (ou même double long), et des chaînes de caractères en caractères normaux ou larges .

En C++, nous avions la possibilité de créer nos propres types (c'est-à-dire les classes), avec potentiellement pas de surcharge (inline, etc.). Nous avons eu la possibilité d'ajouter des opérateurs à leurs types, de les faire se comporter comme des types intégrés similaires, ce qui permet aux développeurs C++ d'utiliser les matrices et les nombres complexes aussi naturellement qu'ils l'auraient fait s'ils avaient été ajoutés au langage lui-même. Nous pouvons même ajouter des opérateurs de cast (ce qui est généralement une mauvaise idée, mais parfois, c'est juste la bonne solution).

Nous avons encore manqué une chose pour que les types d'utilisateurs se comportent comme des types intégrés: les littéraux définis par l'utilisateur.

Donc, je suppose que c'est une évolution naturelle pour le langage, mais pour être aussi complet que possible: " Si vous voulez créer un type, et vous voulez qu'il se comporte autant que possible comme un type intégré, ici sont les outils ... "

Je suppose que c'est très similaire à la décision de .NET de faire de chaque primitive une structure, y compris les booléens, les entiers, etc., et que toutes les structures dérivent d'Object. Cette décision à elle seule met .NET bien au-delà de la portée de Java lorsque vous travaillez avec des primitives, quel que soit le nombre de hacks de boxe/unboxing Java ajoutera à ses spécifications.

En avez-vous vraiment besoin en C++?

Cette question est pour [~ # ~] vous [~ # ~] de répondre. Pas Bjarne Stroustrup. Pas Herb Sutter. Pas n'importe quel membre du comité standard C++. C'est pourquoi vous avez le choix en C++ , et ils ne restreindront pas une notation utile aux seuls types intégrés.

Si vous en avez besoin, alors c'est un ajout bienvenu. Si vous ne le faites pas, eh bien ... Ne l'utilisez pas. Cela ne vous coûtera rien.

Bienvenue dans C++, le langage où les fonctionnalités sont facultatives.

Gonflé??? Montrez-moi vos complexes !!!

Il y a une différence entre gonflé et complexe (jeu de mots voulu).

Comme l'a montré Niels à Quelles nouvelles capacités les littéraux définis par l'utilisateur ajoutent-ils à C++? , être capable d'écrire un nombre complexe est l'une des deux fonctionnalités ajoutées "récemment" à C et C++:

// C89:
MyComplex z1 = { 1, 2 } ;

// C99: You'll note I is a macro, which can lead
// to very interesting situations...
double complex z1 = 1 + 2*I;

// C++:
std::complex<double> z1(1, 2) ;

// C++11: You'll note that "i" won't ever bother
// you elsewhere
std::complex<double> z1 = 1 + 2_i ;

Désormais, le type C99 "double complexe" et le type C++ "std :: complex" peuvent être multipliés, ajoutés, soustraits, etc., en utilisant une surcharge d'opérateur.

Mais dans C99, ils ont juste ajouté un autre type en tant que type intégré et un support de surcharge d'opérateur intégré. Et ils ont ajouté une autre fonctionnalité littérale intégrée.

En C++, ils ont simplement utilisé les fonctionnalités existantes du langage, ont vu que la fonctionnalité littérale était une évolution naturelle du langage, et l'ont donc ajoutée.

En C, si vous avez besoin de la même amélioration de notation pour un autre type, vous n'avez pas de chance jusqu'à ce que votre lobbying ajoute vos fonctions d'onde quantique (ou points 3D, ou tout type de base que vous utilisez dans votre domaine de travail) au La norme C en tant que type intégré réussit.

En C++ 11, vous pouvez simplement le faire vous-même:

Point p = 25_x + 13_y + 3_z ; // 3D point

Est-il gonflé? Non , le besoin est là, comme le montre la façon dont les complexes C et C++ ont besoin d'un moyen de représenter leurs valeurs complexes littérales.

Est-il mal conçu? Non , il est conçu comme toutes les autres fonctionnalités C++, avec une extensibilité à l'esprit.

Est-ce uniquement à des fins de notation? Non , car cela peut même ajouter une sécurité de type à votre code.

Par exemple, imaginons un code orienté CSS:

css::Font::Size p0 = 12_pt ;       // Ok
css::Font::Size p1 = 50_percent ;  // Ok
css::Font::Size p2 = 15_px ;       // Ok
css::Font::Size p3 = 10_em ;       // Ok
css::Font::Size p4 = 15 ;         // ERROR : Won't compile !

Il est alors très facile d'imposer un typage fort à l'affectation des valeurs.

Est-ce dangereux?

Bonne question. Ces fonctions peuvent-elles être espacées? Si oui, alors Jackpot!

Quoi qu'il en soit, comme tout, vous pouvez vous tuer si un outil n'est pas utilisé correctement . C est puissant, et vous pouvez tirer votre tête si vous abusez du pistolet C. C++ a le pistolet C, mais aussi le scalpel, le taser et tout autre outil que vous trouverez dans la boîte à outils. Vous pouvez mal utiliser le scalpel et vous saigner à mort. Ou vous pouvez créer du code très élégant et robuste.

Donc, comme toutes les fonctionnalités C++, en avez-vous vraiment besoin? C'est la question à laquelle vous devez répondre avant de l'utiliser en C++. Sinon, cela ne vous coûtera rien. Mais si vous en avez vraiment besoin, au moins, la langue ne vous décevra pas.

L'exemple de date?

Votre erreur, me semble-t-il, est que vous mélangez des opérateurs:

1974/01/06AD
    ^  ^  ^

Cela ne peut pas être évité, car/étant un opérateur, le compilateur doit l'interpréter. Et, AFAIK, c'est une bonne chose.

Pour trouver une solution à votre problème, j'écrirais le littéral d'une autre manière. Par exemple:

"1974-01-06"_AD ;   // ISO-like notation
"06/01/1974"_AD ;   // french-date-like notation
"jan 06 1974"_AD ;  // US-date-like notation
19740106_AD ;       // integer-date-like notation

Personnellement, je choisirais l'entier et les dates ISO, mais cela dépend de VOS besoins. C'est tout l'intérêt de laisser l'utilisateur définir ses propres noms littéraux.

190
paercebal

C'est très bien pour le code mathématique. De mon esprit, je peux voir l'utilisation des opérateurs suivants:

deg pour les degrés. Cela rend l'écriture des angles absolus beaucoup plus intuitive.

double operator ""_deg(long double d)
{ 
    // returns radians
    return d*M_PI/180; 
}

Il peut également être utilisé pour diverses représentations en virgule fixe (qui sont toujours utilisées dans le domaine des DSP et des graphiques).

int operator ""_fix(long double d)
{ 
    // returns d as a 1.15.16 fixed point number
    return (int)(d*65536.0f); 
}

Cela ressemble à de bons exemples d'utilisation. Ils aident à rendre les constantes du code plus lisibles. C'est un autre outil pour rendre le code illisible également, mais nous avons déjà tellement d'outils abusifs qu'un de plus ne fait pas grand mal.

36
Nils Pipenbrinck

Les UDL sont à espace de noms (et peuvent être importés à l'aide de déclarations/directives, mais vous ne pouvez pas explicitement nommer un littéral comme 3.14std::i), ce qui signifie (espérons-le) qu'il n'y aura pas une tonne d'affrontements.

Le fait qu'ils puissent être modélisés (et constexpr'd) signifie que vous pouvez faire des choses assez puissantes avec les UDL. Les auteurs de Bigint seront vraiment heureux, car ils peuvent enfin avoir des constantes arbitrairement grandes, calculées au moment de la compilation (via constexpr ou des modèles).

Je suis juste triste que nous ne verrons pas quelques littéraux utiles dans la norme (à en juger par son apparence), comme s pour std::string et i pour l'unité imaginaire.

La quantité de temps de codage qui sera économisée par les UDL n'est en fait pas si élevée, mais la lisibilité sera considérablement augmentée et de plus en plus de calculs peuvent être transférés au moment de la compilation pour une exécution plus rapide.

17
coppro

Permettez-moi d'ajouter un peu de contexte. Pour notre travail, les littéraux définis par l'utilisateur sont indispensables. Nous travaillons sur MDE (Model-Driven Engineering). Nous voulons définir des modèles et des métamodèles en C++. Nous avons en fait implémenté un mappage d'Ecore vers C++ ( EMF4CPP ).

Le problème survient lorsqu'il est possible de définir des éléments de modèle en tant que classes en C++. Nous adoptons l'approche de la transformation du métamodèle (Ecore) en modèles avec arguments. Les arguments du modèle sont les caractéristiques structurelles des types et des classes. Par exemple, une classe avec deux attributs int serait quelque chose comme:

typedef ::ecore::Class< Attribute<int>, Attribute<int> > MyClass;

Cependant, il s'avère que chaque élément d'un modèle ou métamodèle a généralement un nom. Nous aimerions écrire:

typedef ::ecore::Class< "MyClass", Attribute< "x", int>, Attribute<"y", int> > MyClass;

MAIS, C++ ou C++ 0x ne le permettent pas, car les chaînes sont interdites en tant qu'arguments aux modèles. Vous pouvez écrire le nom char par char, mais c'est certainement un gâchis. Avec des littéraux définis par l'utilisateur appropriés, nous pourrions écrire quelque chose de similaire. Disons que nous utilisons "_n" pour identifier les noms des éléments du modèle (je n'utilise pas la syntaxe exacte, juste pour faire une idée):

typedef ::ecore::Class< MyClass_n, Attribute< x_n, int>, Attribute<y_n, int> > MyClass;

Enfin, avoir ces définitions comme modèles nous aide beaucoup à concevoir des algorithmes pour parcourir les éléments du modèle, les transformations de modèle, etc. qui sont vraiment efficaces, car les informations de type, l'identification, les transformations, etc. sont déterminées par le compilateur au moment de la compilation.

12
Diego Sevilla

Bjarne Stroustrup parle des UDL dans ce C++ 11 talk , dans la première section sur les interfaces riches en types, environ 20 minutes.

Son argument de base pour les UDL prend la forme d'un syllogisme:

  1. Les types "triviaux", c'est-à-dire les types primitifs intégrés, ne peuvent détecter que les erreurs de type triviales. Les interfaces avec des types plus riches permettent au système de types de détecter plus de types d'erreurs.

  2. Les types d'erreurs de type que le code richement typé peut détecter ont un impact sur le code réel. (Il donne l'exemple de Mars Climate Orbiter, qui a tristement échoué en raison d'une erreur de dimensions dans une constante importante).

  3. En code réel, les unités sont rarement utilisées. Les gens ne les utilisent pas, car l'exécution de calculs d'exécution ou de surcharge de mémoire pour créer des types riches est trop coûteuse, et l'utilisation d'un code d'unité C++ pré-existant est si horriblement malsaine que personne ne l'utilise. (Empiriquement, personne ne l'utilise, même si les bibliothèques existent depuis une décennie).

  4. Par conséquent, afin d'amener les ingénieurs à utiliser des unités dans du code réel, nous avions besoin d'un périphérique qui (1) n'encourage aucun temps d'exécution et (2) est acceptable sur le plan de la notation.

10
masonk

La prise en charge de la vérification des dimensions à la compilation est la seule justification requise.

auto force = 2_N; 
auto dx = 2_m; 
auto energy = force * dx; 

assert(energy == 4_J); 

Voir par exemple PhysUnits-CT-Cpp11 , une petite bibliothèque uniquement en-tête C++ 11, C++ 14 pour l'analyse dimensionnelle au moment de la compilation et la manipulation et la conversion d'unités/quantités. Plus simple que Boost.Units , prend en charge symbole d'unité littéraux tels que m, g, s, préfixes métriques tels que m, k, M, dépend uniquement de la bibliothèque C++ standard, SI uniquement, des puissances intégrales des dimensions.

8
Martin Moene

Hmm ... Je n'ai pas encore pensé à cette fonctionnalité. Votre échantillon a été bien pensé et est certainement intéressant. Le C++ est très puissant tel qu'il est actuellement, mais malheureusement la syntaxe utilisée dans les morceaux de code que vous lisez est parfois trop complexe. La lisibilité est, sinon tout, du moins beaucoup. Et une telle fonctionnalité serait conçue pour plus de lisibilité. Si je prends votre dernier exemple

assert(1_kg == 2.2_lb); // give or take 0.00462262 pounds

... Je me demande comment vous exprimeriez cela aujourd'hui. Vous auriez une classe KG et une classe LB et vous compareriez des objets implicites:

assert(KG(1.0f) == LB(2.2f));

Et cela ferait aussi bien. Avec des types qui ont des noms plus longs ou des types que vous n'avez aucun espoir d'avoir un tel constructeur Nice pour sans écrire un adaptateur, cela pourrait être un ajout agréable pour la création et l'initialisation d'objet implicite à la volée. D'un autre côté, vous pouvez également créer et initialiser des objets à l'aide de méthodes également.

Mais je suis d'accord avec Nils sur les mathématiques. Les fonctions de trigonométrie C et C++ par exemple nécessitent une entrée en radians. Je pense cependant en degrés, donc une conversion implicite très courte comme Nils publiée est très agréable.

En fin de compte, ce sera du sucre syntaxique, mais cela aura un léger effet sur la lisibilité. Et il sera probablement plus facile d'écrire aussi certaines expressions (sin (180.0deg) est plus facile à écrire que sin (deg (180.0)). Et puis il y aura des gens qui abuseront du concept. Mais alors, les personnes abusives du langage devraient utiliser langages très restrictifs plutôt que quelque chose d'aussi expressif que C++.

Ah, mon post ne dit pratiquement rien sauf: ça va aller, l'impact ne sera pas trop grand. Ne vous inquiétez pas. :-)

6
mstrobl

Je n'ai jamais eu besoin ou voulu cette fonctionnalité (mais cela pourrait être l'effet Blub ). Ma réaction de genou est qu'elle est boiteuse et susceptible de plaire aux mêmes personnes qui pensent que c'est cool de surcharger l'opérateur + pour toute opération qui pourrait à distance être interprétée comme une addition.

3
fizzer

C++ est généralement très strict sur la syntaxe utilisée - sauf le préprocesseur, vous ne pouvez pas utiliser grand-chose pour définir une syntaxe/grammaire personnalisée. Par exemple. nous pouvons surcharger les opéras existants, mais nous ne pouvons pas en définir de nouveaux - IMO c'est très en accord avec l'esprit de C++.

Cela ne me dérange pas de trouver un code source plus personnalisé - mais le point choisi me semble très isolé, ce qui me trouble le plus.

Même l'utilisation prévue peut rendre la lecture du code source beaucoup plus difficile: une seule lettre peut avoir des effets secondaires de grande envergure qui ne peuvent en aucun cas être identifiés à partir du contexte. Avec une symétrie à u, l et f, la plupart des développeurs choisiront des lettres simples.

Cela peut également transformer la portée en problème, l'utilisation de lettres simples dans l'espace de noms global sera probablement considérée comme une mauvaise pratique, et les outils qui sont censés mélanger plus facilement les bibliothèques (espaces de noms et identificateurs descriptifs) iront probablement à l'encontre de son objectif.

Je vois un certain mérite en combinaison avec "auto", également en combinaison avec une bibliothèque d'unités comme boost unités , mais pas assez pour mériter cette addition.

Je me demande cependant quelles idées intelligentes nous proposons.

2
peterchen

J'ai utilisé des littéraux utilisateur pour des chaînes binaires comme ceci:

 "asd\0\0\0\1"_b

en utilisant le constructeur std::string(str, n) pour que \0 ne coupe pas la chaîne en deux. (Le projet fait beaucoup de travail avec différents formats de fichiers.)

Cela a également été utile lorsque j'ai abandonné std::string En faveur d'un emballage pour std::vector.

2
rr-