web-dev-qa-db-fra.com

Comment fonctionnent les variables en ligne?

Lors de la réunion 2016 sur les normes ISO C++ d'Oulu, une proposition intitulée Inline Variables a été adoptée en C++ 17 par le comité de normalisation.

En termes simples, en quoi consistent les variables en ligne, comment fonctionnent-elles et à quoi servent-elles? Comment les variables en ligne doivent-elles être déclarées, définies et utilisées?

99
jotik

La première phrase de la proposition:

Le spécificateur inline peut être appliqué aux variables ainsi qu'aux fonctions.

L’effet ¹ garanti de inline appliqué à une fonction est de permettre à la fonction d’être définie de manière identique, avec une liaison externe, en plusieurs unités de traduction. En pratique, cela signifie que vous définissez la fonction dans un en-tête, vous pouvez l'inclure dans plusieurs unités de traduction. La proposition étend cette possibilité aux variables.

Ainsi, dans la pratique, la proposition (désormais acceptée) vous permet d’utiliser le mot clé inline pour définir une liaison externe const variable de la portée de l’espace de nommage ou tout membre de données de la classe static, dans un en-tête. fichier, de sorte que les définitions multiples résultant lorsque cet en-tête est inclus dans plusieurs unités de traduction conviennent à l'éditeur de liens - il en choisit simplement un .

Jusqu'en C++ 14 inclus, la machinerie interne était là pour prendre en charge les variables static dans les modèles de classe, mais il n'existait aucun moyen pratique d'utiliser cette machinerie. Il fallait recourir à des astuces comme

template< class Dummy >
struct Kath_
{
    static std::string const hi;
};

template< class Dummy >
std::string const Kath_<Dummy>::hi = "Zzzzz...";

using Kath = Kath_<void>;    // Allows you to write `Kath::hi`.

À partir de C++ 17 et après, je pense que l’on peut écrire simplement

struct Kath
{
    static std::string const hi;
};

inline std::string const Kath::hi = "Zzzzz...";    // Simpler!

… Dans un fichier d'en-tête.

La proposition comprend la formulation

Un membre de données statique en ligne peut être défini dans la définition de classe et peut spécifier un initialisateur entre accolades ou égales. Si le membre est déclaré avec le spécificateur constexpr, il peut être redéclaré dans la portée de l'espace de noms sans initialiseur (cet usage est déconseillé; voir section D.X). Les déclarations d’autres membres de données statiques ne doivent pas spécifier un identificateur de type accolade ou égal

… Ce qui permet de simplifier davantage ce qui précède

struct Kath
{
    static inline std::string const hi = "Zzzzz...";    // Simplest!
};

… Comme noté par T.C dans n commentaire à cette réponse.

De plus, le spécificateur ​constexpr implique inline pour les membres de données statiques ainsi que pour les fonctions.


Remarques:
¹ Pour une fonction inline a également un effet suggestif sur l'optimisation, le compilateur préférant remplacer les appels de cette fonction par une substitution directe du code machine de la fonction. Cette allusion peut être ignorée.

103

Les variables en ligne sont très similaires aux fonctions en ligne. Il signale à l'éditeur de liens qu'il ne doit exister qu'une seule instance de la variable, même si la variable est vue dans plusieurs unités de compilation. L'éditeur de liens doit s'assurer qu'aucune autre copie n'est créée.

Les variables en ligne peuvent être utilisées pour définir des éléments globaux dans des bibliothèques contenant uniquement des en-têtes. Avant C++ 17, ils devaient utiliser des solutions de contournement (fonctions inline ou modèles de hacks).

Par exemple, une solution de contournement consiste à utiliser le singleton de Meyer avec une fonction inline:

inline T& instance()
{
  static T global;
  return global;
}

Cette approche présente certains inconvénients, principalement en termes de performances. Cet encombrement pourrait être évité par des solutions de modèle, mais il est facile de se tromper.

Avec les variables en ligne, vous pouvez le déclarer directement (sans générer d'erreur de l'éditeur de liens à définitions multiples):

inline T global;

En dehors des bibliothèques contenant uniquement des en-têtes, il existe d'autres cas où les variables en ligne peuvent aider. Nir Friedman aborde ce sujet dans son exposé à la CppCon: Ce que les développeurs C++ devraient savoir sur les globals (et l’éditeur de liens)) . La partie sur les variables en ligne et les solutions de contournement commence à 18m9s .

En résumé, si vous devez déclarer des variables globales partagées entre des unités de compilation, leur déclaration en tant que variables en ligne dans le fichier d'en-tête est simple et évite les problèmes liés aux solutions de contournement antérieures à C++ 17.

(Il existe encore des cas d'utilisation du singleton de Meyer, par exemple, si vous souhaitez explicitement avoir une initialisation lente.)

11
Philipp Claßen

Exemple minimal exécutable

Cette fonctionnalité géniale de C++ 17 nous permet de:

  • utilisez simplement une seule adresse mémoire pour chaque constante
  • enregistrez-le sous le nom constexpr: Comment déclarer constexpr extern?
  • faites-le en une seule ligne d'un en-tête

main.cpp

#include <cassert>

#include "notmain.hpp"

int main() {
    // Both files see the same memory address.
    assert(&notmain_i == notmain_func());
    assert(notmain_i == 42);
}

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

inline constexpr int notmain_i = 42;

const int* notmain_func();

#endif

notmain.cpp

#include "notmain.hpp"

const int* notmain_func() {
    return &notmain_i;
}

Compiler et exécuter:

g++ -c -o notmain.o -std=c++17 -Wall -Wextra -pedantic notmain.cpp
g++ -c -o main.o -std=c++17 -Wall -Wextra -pedantic main.cpp
g++ -o main -std=c++17 -Wall -Wextra -pedantic main.o notmain.o
./main

GitHub en amont .

Voir aussi: Comment fonctionnent les variables en ligne?

Norme C++ sur les variables en ligne

La norme C++ garantit que les adresses seront les mêmes. Projet de norme C++ 17 N4659 10.1.6 "Le spécificateur en ligne":

6 Une fonction ou une variable en ligne avec une liaison externe doit avoir la même adresse dans toutes les unités de traduction.

cppreference https://en.cppreference.com/w/cpp/language/inline explique que si static n'est pas renseigné, il est doté d'un lien externe.

Implémentation de la variable en ligne GCC

Nous pouvons observer comment cela est implémenté avec:

nm main.o notmain.o

qui contient:

main.o:
                 U _GLOBAL_OFFSET_TABLE_
                 U _Z12notmain_funcv
0000000000000028 r _ZZ4mainE19__PRETTY_FUNCTION__
                 U __assert_fail
0000000000000000 T main
0000000000000000 u notmain_i

notmain.o:
0000000000000000 T _Z12notmain_funcv
0000000000000000 u notmain_i

et man nm dit à propos de u:

"u" Le symbole est un symbole global unique. Il s’agit d’une extension GNU de l’ensemble standard de liaisons de symboles ELF. Pour un tel symbole, l’éditeur de liens dynamique s’assurera que dans tout le processus, il n’ya qu’un symbole portant ce nom et ce type en cours d’utilisation.

nous voyons donc qu’il existe une extension ELF dédiée à cela.

Pré-C++ 17: extern const

Avant C++ 17 et en C, nous pouvons obtenir un effet très similaire avec un extern const, ce qui conduira à l’utilisation d’un seul emplacement mémoire.

Les inconvénients par rapport à inline sont les suivants:

  • avec cette technique, il n’est pas possible de créer la variable constexpr, seul inline permet que: Comment déclarer constexpr extern?
  • il est moins élégant car vous devez déclarer et définir la variable séparément dans l'en-tête et le fichier cpp

main.cpp

#include <cassert>

#include "notmain.hpp"

int main() {
    // Both files see the same memory address.
    assert(&notmain_i == notmain_func());
    assert(notmain_i == 42);
}

notmain.cpp

#include "notmain.hpp"

const int notmain_i = 42;

const int* notmain_func() {
    return &notmain_i;
}

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

extern const int notmain_i;

const int* notmain_func();

#endif

GitHub en amont .

En-tête pré-C++ 17 uniquement alternatives

Celles-ci ne sont pas aussi bonnes que la solution extern, mais elles fonctionnent et n'occupent qu'un seul emplacement de mémoire:

Une fonction constexpr, car constexpr IMPLIQUE inline et inlineautorise (force) la définition à apparaître sur chaque unité de traduction :

constexpr int shared_inline_constexpr() { return 42; }

et je parie que tout compilateur décent s'inscrira dans l'appel.

Vous pouvez également utiliser une variable statique const ou constexpr comme dans:

#include <iostream>

struct MyClass {
    static constexpr int i = 42;
};

int main() {
    std::cout << MyClass::i << std::endl;
    // undefined reference to `MyClass::i'
    //std::cout << &MyClass::i << std::endl;
}

mais vous ne pouvez pas prendre son adresse, ou bien il devient odr-used, voir aussi: https://en.cppreference.com/w/cpp/language/static "Membres statiques constants" et Définition des membres de données statiques constexpr

C

En C, la situation est identique à celle de C++ pré C++ 17, un exemple a été téléchargé à l'adresse suivante: Que signifie "statique" en C?

La seule différence est qu'en C++, const implique static pour les globals, mais pas en C: Sémantique C++ de `static const` vs` const`

Y at-il un moyen de l’intégrer complètement?

TODO: y a-t-il un moyen de complètement intégrer la variable, sans utiliser de mémoire du tout?

Cela ressemble beaucoup à ce que fait le pré-processeur.

Cela nécessiterait en quelque sorte:

  • interdire ou détecter si l'adresse de la variable est prise
  • ajouter cette information aux fichiers objet ELF et laisser LTO l'optimiser

Apparenté, relié, connexe:

Testé sous Ubuntu 18.10, GCC 8.2.0.