litéraux, rvalue or lvalue ?

Euh ... je suis un peu perdu, ...

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Dans une réponse à un de mes posts, jo_link_noir me dit :

Attention avec les littéraux, seuls les numériques sont des rvalues, les littéraux de chaîne sont des lvalue. Mais "str"s est une std::string qui est bien une rvalue.

jo_link_noir

Je ne comprends pas cette distinction entre d’une part les numériques et les std::string, et d’autre part les chaîne de caractères.

Est-ce-que quelqu’un peut m’expliquer ?

Merci d’avance.

+0 -0

D’après ce que je comprend de son message.

  • 11 => rvalue
  • "str"s => rvalue
  • "str" => lvalue O_o

Je ne comprend pas l’intérêt.

+0 -0

La distinction est à faire entre "..." qui est un littéral de chaîne (char const[n]) et "..."s qui est l’utilisation de operator""s qui retourne std::string, ce qui revient à utiliser une fonction.

C’était surtout par rapport au commentaire: le test n’est pas sur un littéral.

Pour l’explication sur "": le pointeur peut être partagé par toutes les chaînes identiques et celles-ci vivent en dehors du scope dans lequel elles sont déclarées. Du coup, c’est un espace mémoire en lecture seulement accessible tout le temps et absolument pas temporaire.

+0 -0

C’est inutile d’essayer de comprendre. Ce type de choix est souvent un mélange de raisons historiques, de rétrocompatibilité du code, de détails obscures que peu de gens comprennent. Et c’est généralement inutile de connaitre ce genre de détails (sauf si vous bosser sur un compilateur C++ ou une lib très bas niveau comme la STL ou boost).

La simplification que l’on utilise habituellement (si on peut écrire &x, c’est a dire que l’objet a une adresse mémoire, c’est une lvalue. Sinon c’est une rvalue) est suffisante pour comprendre et coder.

+0 -0

Pour l’explication sur "": le pointeur peut être partagé par toutes les chaînes identiques et celles-ci vivent en dehors du scope dans lequel elles sont déclarées. Du coup, c’est un espace mémoire en lecture seulement accessible tout le temps et absolument pas temporaire.

jo_link_noir

Du coup, si c’est en lecteur seul, quel est l’intérêt d’être une lvalue ? 😵

+0 -0

Du coup, si c’est en lecteur seul, quel est l’intérêt d’être une lvalue ? 😵

Ça ne peut pas être une rvalue: le littéral de chaîne a une adresse (fixe) et les données sont const pour qu’on ne puisse pas le modifier. Si ce n’était pas constant, en écrivant foo("abc"), le paramètre de foo pourrait magiquement devenir "012" parce que quelqu’un la modifiée.

J’ai l’impression que parce que c’est constant, tu préférerais une rvalue. Mais une rvalue constante est plutôt inutile: on est autorisé à faire ce que l’on veut avec, sauf modifier son état… c’est contradictoire. Les rvalues ne sont utiles que pour y vampiriser les données en les modifiant. Dans le cas contraire, ce n’est pas mieux qu’une référence constante.

Du coup, si c’est en lecteur seul, quel est l’intérêt d’être une lvalue ? 😵

Ça ne peut pas être une rvalue: le littéral de chaîne a une adresse (fixe) et les données sont const pour qu’on ne puisse pas le modifier.

Je ne vois pas en quoi ça contredit le fait que ça puisse être une rvalue.

J’ai l’impression que parce que c’est constant, tu préférerais une rvalue.

Non, ça je m’en fiche, un tableau est constant et est une lvalue. Ce qui me dérange vraiment c’est qu’une littéral ne se trouve jamais à gauche d’une affectation. Donc ça devrait être une rvalue.

+0 -0

une littéral ne se trouve jamais à gauche d’une affectation. Donc ça devrait être une rvalue.

ache

C’est la définition historique de lvalue. Mais c’est pas la définition actuelle. "être a gauche d’une affectation" est juste un cas particulier de lvalue dans la definition actuelle.

A modifiable lvalue may be used as the left-hand operand of the built-in assignment and compound assignment operators.

Cf les références données dans le lien sur cppreferences pour les détails de ce choix.

+0 -0

Bonjour, et merci pour vos réponses,

Si je résume ce que j’ai compris de vos réponses:

La distinction est à faire entre "..." qui est un littéral de chaîne (char const[n]) et "..."s qui est l’utilisation de operator""s qui retourne std::string, ce qui revient à utiliser une fonction.

jo_link_noir

Ok, je n’avais pas compris la subtilité de la syntaxe ""s

C’est inutile d’essayer de comprendre. …

gbdivers

Ok, ca aussi, c’est clair :D , mais bon, si j’ai fait un post, c’est pour essayer de comprendre pour essayer d’utiliser!

Bon si je résume ce que j’ai à appliquer:

  • Classe à sémantique de donnée: la règle du "5 ou rien", on met "= default" pour les 5 membres, ou on les ré-écrit!
  • Classe à sémantique d’entité: On met "= 0 delete", au copy/move constructeur et copy/move assigement.

Ensuite on écrit son code, et le compilateur optimise les copy/move. (on ne se pose pas de question sur l’optimisation des littéraux, le compilateur faut au mieux)

Enfin, quand il faut optimiser, on se pose la question de localisation des variable (tas ou pile), afin de rendre éventuellement le move plus efficace.

Est-ce que c’est un résumé pas trop con ?

Edit: Oups! … delete oui, pas 0 ! Merci !

Cordialement.

+0 -0

c’est pour essayer de comprendre pour essayer d’utiliser!

Dedeun

Oui, mais au final, ton résumé est complètement indépendant des explications que tu as eu sur qui et pourquoi est une lvalue ou rvalue. Et c’est bien pour cela que c’est osef au final (dans la pratique de tous les jours). Honnêtement, c’est un truc qui m’était complètement sorti de la tete que les littérales string sont des lvalues. Et je parie que si on demande aux devs C++ expert, la plus part feront des erreurs.

Tu peux faire un petit exo pour le fun : essaie d’écrire un code (un minimum réaliste) qui permet de vérifier que les littérales string sont des lvalue (c’est a dire un code qui assumerait que les littérales string sont des rvalue et qui plante parce que ce sont des lvalue).

Quand on apprend un langage, on a tendance a se perdre dans les détails syntaxiques, en oubliant de se demander a quoi ça sert et si c’est utile.

Bon si je résume ce que j’ai à appliquer:

  • Classe à sémantique de donnée: la règle du "5 ou rien", on met "= default" pour les 5 membres, ou on les ré-écrit!
  • Classe à sémantique d’entité: On met "= 0", au copy/move constructeur et copy/move assigement.
Dedeun
  • sémantique de données" -> "sémantique de valeur" ("value type" en anglais)
  • sémantique d’entité… disons qu’il y a débat sur le terme anglais :)

La seule vraie règle a priori, c’est pas de copie pour les entités. Le reste, cela va dépendre de ce que tu veux que ta classe fasse. Un exemple con : si tu implémentes un singleton avec une sémantique de valeur, tu ne vas pas autoriser la copie. Ça n’aurait pas de sens. (Bouh, c’est mal les singleton !)

Pour le déplacement… certains considèrent que c’est une forme de copie, donc interdit dans une entité. On aime bien débattre en C++ :D

Hors sujet : pourquoi =0 ? Tu voulais dire = delete ?

Enfin, quand il faut optimiser, on se pose la question de localisation des variable (tas ou pile), afin de rendre éventuellement le move plus efficace.

Dedeun

Ou alors, tu laisses les variables ou elles sont et tu supprimes les moves…

Le probleme de l’optimisation, c’est qu’il faut savoir quoi optimiser (2). Et ça, on ne peut le faire qu’avec des mesures précises (donc du profiling), dans une utilisation réaliste (1).

(1) Parce que souvent les optimisations dépendent des données que l’on manipule. Donc faire une analyse sur des données random, c’est une mesure parfois imprécise ou fausse.

Un exemple classique, c’est les algos de tri. Certains sont plus efficace sur peu de données, d’autres sur des données de grosse taille, d’autre en multithread, d’autres quand les données sont pre-triées, etc. On ne peut pas dire qu’un algo est meilleur dans l’absolu (au mieux, on dit qu’un algo - en général quicksort - est le meilleur compromis).

(2) On peut optimiser pleins de choses :

  • les structures de données et les algos (ajouter un tableau de hachage ? Changer un SoA en AoS ou l’inverse ? etc)
  • la gestion des caches (données, instructions)
  • Les branch prediction fails
  • les allocations des données, les copies, etc
  • les appels de fonctions
  • etc

Partir a priori sur une question de Pile versus Tas sans mesure, c’est partir au pifomètre, parce qu’on ne sait pas le poids de chaque problèmes dans les performances finales. Perdre du temps a changer par exemple la Pile/Tas si cela prend 1% des perfs et que tu as a coté un probleme de cache qui te bouffe 50%, c’est une perte de temps.

Et il faut aussi prendre en compte le cout pour mettre en place une solution par rapport au gain. Si ajouter des threads prend 6 mois et que cela te fait gagner 2% de perf et que corriger une structure de données prend 1 jour et te fait gagner que 1%, c’est probablement mieux de faire la seconde solution en premier.

Et c’est le gain ressenti par l’utilisateur. Le gain subjectif. Parce que prendre 6 mois pour développer une solution qui te fait gagner 1% (donc par exemple passer de 60fps a 60,6%…), tout le monde s’en fout.

(Quand on est expérimenté, on a un peu plus de pifomètre et on a une idée de ce qu’on va pouvoir optimiser sans mesure. Mais il ne faut jamais oublier que cela reste du pifomètre)

Tout ça pour dire, au final, que quand tu prends en considération le fait qu’on ne sait pas trop a quoi ça sert de savoir exactement la différence entre lvalue et rvalue (sauf domaines très spécifiques) et que cela n’aura pas forcement une utilité immédiate pour optimiser, c’est un truc largement oubliable.

+0 -0

Tu peux faire un petit exo pour le fun : essaie d’écrire un code (un minimum réaliste) qui permet de vérifier que les littérales string sont des lvalue (c’est a dire un code qui assumerait que les littérales string sont des rvalue et qui plante parce que ce sont des lvalue).

Euh ben du coup:

#define ALPHA "AB"

// [..]
  for(int i = 0 ; i < sizeof ALPHA ; i++) {
      putchar(ALPHA[i]
  }

Ok, c’est du C.

+0 -0

Ce code passe sans probleme en C++ (clang 9).

Ca serait quoi l’erreur attendue en C ? (Je ne connais pas assez le C, pour moi le code semble valide).

EDIT : teste sur Wandbox avec GCC et Clang, le code est valide en C aussi. Avec un beau warning, c’est moche d’utiliser int sur un tableau…

Start
prog.c: In function 'main':
prog.c:6:21: warning: comparison of integer expressions of different 
signedness: 'int' and 'long unsigned int' [-Wsign-compare]
    6 |   for(int i = 0 ; i < sizeof ALPHA ; i++) {
      |                     ^
AB
0
Finish
+0 -0

Ben du coup, il marche (si on rajoute la parenthèse).

Mais par-contre, si les littéraux sont des rvalues alors ALPHA ne peut pas être un tableau et donc sizeof ne peut pas retourner la taille de la chaîne.

Je me rend compte que c’est le contraire de ce que tu as cherche. 🤔
Tu veux un truc qui plante quand on le compile car à l’écriture on part du principe que c’est un rvalue. Mais si on part du principe que c’est une rvalue alors on va pas utiliser sizeof ni tenter quoi que ce soit dessus en fait … Donc peut importe ce qu’on fait ça sera pas réaliste.

Par exemple sizeof("") pour chercher la taille d’un pointeur (car on part du principe que c’est une rvalue donc char*) alors que c’est un tableau de taille 1.

Un code qui plante sera forcément aussi tordu que ça. 🤔

+0 -0

Mais si on part du principe que c’est une rvalue alors on va pas utiliser sizeof

Pourquoi ? Que se soit une rvalue ou une lvalue, c’est du pareil au même pour sizeof.

Par exemple sizeof("") pour chercher la taille d’un pointeur (car on part du principe que c’est une rvalue donc char*) alors que c’est un tableau de taille 1.

On peut très bien avoir une rvalue sur des tableaux.

template<class X, std::size_t N>
void foo(X(&&x)[N]);

foo({1,2});

Tu mélanges le type et les références, les 2 ne sont absolument pas liés. Le fait que char[n] puisse rentrer dans un char*&& est dû à une conversion implicite. Une conversion vers un type T donne forcément un temporaire.

void foo(long&& x);

int i;
foo(i); // x est une rvalue alors que i est une lvalue
        // -> la valeur de type long n'existe que pendant l'appel de la fonction

struct A{};
struct B{ B(const A&); };
void bar(B&&);

A a;
bar(a); // idem, la règle ne change pas
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte