¿Cómo funcionan las variables en línea?

124

En la reunión de estándares 2016 de Oulu ISO C ++, el comité de estándares votó una propuesta llamada Variables en línea en C ++ 17.

En términos simples, ¿qué son las variables en línea, cómo funcionan y para qué sirven? ¿Cómo deben declararse, definirse y utilizarse las variables en línea?

jotik
fuente
@jotik Supongo que la operación equivalente reemplazaría cualquier ocurrencia de la variable por su valor. Normalmente esto solo es válido si la variable es const.
melpomene
55
Eso no es lo único que hace la inlinepalabra clave para las funciones. La inlinepalabra clave, cuando se aplica a las funciones, tiene otro efecto crucial, que se traduce directamente en variables. Una inlinefunción, que presumiblemente se declara en un archivo de encabezado, no dará como resultado errores de "símbolo duplicado" en el momento del enlace, incluso si el encabezado obtiene #included por múltiples unidades de traducción. La inlinepalabra clave, cuando se aplica a las variables, tendrá el mismo resultado exacto. El fin.
Sam Varshavchik
44
^ En el sentido de 'sustituir cualquier llamada a esta función con una copia local de su código', inlinees solo una solicitud débil y no vinculante para el optimizador. Los compiladores son libres de no solicitar funciones en línea y / o de funciones que no haya anotado. Más bien, el propósito real de la inlinepalabra clave es eludir múltiples errores de definición.
underscore_d

Respuestas:

121

La primera oración de la propuesta:

El inlineespecificador se puede aplicar a las variables, así como a las funciones.

El efecto garantizado de inlineaplicado a una función es permitir que la función se defina de forma idéntica, con enlaces externos, en múltiples unidades de traducción. Para la práctica, eso significa definir la función en un encabezado, que se puede incluir en varias unidades de traducción. La propuesta extiende esta posibilidad a las variables.

Entonces, en términos prácticos, la propuesta (ahora aceptada) le permite usar la inlinepalabra clave para definir una constvariable de alcance del espacio de nombres de enlace externo , o cualquier staticmiembro de datos de clase, en un archivo de encabezado, de modo que las múltiples definiciones que resultan cuando ese encabezado se incluye en múltiples unidades de traducción están bien con el enlazador, solo elige una de ellas.

Hasta e incluyendo C ++ 14, la maquinaria interna para esto ha estado allí, con el fin de admitir staticvariables en las plantillas de clase, pero no había una forma conveniente de usar esa maquinaria. Había que recurrir a trucos como

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`.

Desde C ++ 17 en adelante, creo que uno puede escribir solo

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

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

... en un archivo de encabezado.

La propuesta incluye la redacción

Un miembro de datos estáticos en línea se puede definir en la definición de clase y puede especificar un inicializador de paréntesis o igual. Si el miembro se declara con el constexprespecificador, se puede volver a declarar en el ámbito del espacio de nombres sin inicializador (este uso está en desuso; consulte DX). Las declaraciones de otros miembros de datos estáticos no deben especificar un inicializador de llave o igual

... lo que permite que lo anterior se simplifique aún más a solo

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

... como señaló TC en un comentario a esta respuesta.

Además, el  ​constexprespecificador implica tanto  inline para miembros de datos estáticos como para funciones.


Notas:
¹ Para una función inlinetambién tiene un efecto indirecto sobre la optimización, que el compilador debería preferir reemplazar las llamadas de esta función con la sustitución directa del código de máquina de la función. Esta sugerencia puede ser ignorada.

Saludos y hth. - Alf
fuente
2
Además, la restricción constante solo se aplica a las variables de alcance del espacio de nombres. Los de ámbito de clase (como Kath::hi) no tienen que ser constantes.
TC
44
Los informes más recientes indican que la constrestricción se ha eliminado por completo.
TC
2
@Nick: Dado que Richard Smith (el actual "editor del proyecto" del comité de C ++) es uno de los dos autores, y dado que es "el propietario del código de la interfaz de Clang C ++", adivinó Clang. Y la construcción compilada con clang 3.9.0 en Godbolt . Advierte que las variables en línea son una extensión C ++ 1z. No encontré ninguna manera de compartir la fuente y las opciones y opciones del compilador, por lo que el enlace es solo para el sitio en general, lo siento.
Saludos y hth. - Alf
1
¿Por qué se necesita una palabra clave en línea dentro de la declaración de clase / estructura? ¿Por qué no permitir simplemente static std::string const hi = "Zzzzz...";?
sasha.sochka
2
@EmilianCioca: No, te encontrarás con el fiasco del orden de inicialización estática . Un singleton es esencialmente un dispositivo para evitar eso.
Saludos y hth. - Alf
15

Las variables en línea son muy similares a las funciones en línea. Indica al vinculador que solo debe existir una instancia de la variable, incluso si la variable se ve en varias unidades de compilación. El vinculador debe asegurarse de que no se creen más copias.

Las variables en línea se pueden usar para definir globales en bibliotecas de encabezado solamente. Antes de C ++ 17, tenían que usar soluciones alternativas (funciones en línea o hacks de plantillas).

Por ejemplo, una solución alternativa es usar el singleton de Meyer con una función en línea:

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

Hay algunos inconvenientes con este enfoque, principalmente en términos de rendimiento. Las soluciones de plantilla podrían evitar esta sobrecarga, pero es fácil equivocarse.

Con variables en línea, puede declararlo directamente (sin obtener un error de enlazador de definición múltiple):

inline T global;

Además de las bibliotecas de encabezado solamente, existen otros casos en los que las variables en línea pueden ayudar. Nir Friedman cubre este tema en su charla en CppCon: Lo que los desarrolladores de C ++ deben saber sobre los globales (y el enlazador) . La parte sobre las variables en línea y las soluciones comienza a los 18m9s .

Para resumir, si necesita declarar variables globales que se comparten entre las unidades de compilación, declararlas como variables en línea en el archivo de encabezado es sencillo y evita los problemas con soluciones alternativas anteriores a C ++ 17.

(Todavía hay casos de uso para el singleton de Meyer, por ejemplo, si desea explícitamente tener una inicialización diferida).

Philipp Claßen
fuente
11

Ejemplo ejecutable mínimo

Esta increíble característica de C ++ 17 nos permite:

  • utilice convenientemente una sola dirección de memoria para cada constante
  • almacenarlo como constexpr: ¿Cómo declarar constexpr extern?
  • hazlo en una sola línea desde un encabezado

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;
}

Compilar y ejecutar:

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 aguas arriba .

Ver también: ¿Cómo funcionan las variables en línea?

C ++ estándar en variables en línea

El estándar C ++ garantiza que las direcciones serán las mismas. C ++ 17 N4659 borrador estándar 10.1.6 "El especificador en línea":

6 Una función o variable en línea con enlace externo tendrá la misma dirección en todas las unidades de traducción.

cppreference https://en.cppreference.com/w/cpp/language/inline explica que si staticno se proporciona, entonces tiene un enlace externo.

Implementación variable en línea de GCC

Podemos observar cómo se implementa con:

nm main.o notmain.o

que contiene:

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

y man nmdice acerca de u:

"u" El símbolo es un símbolo global único. Esta es una extensión de GNU para el conjunto estándar de enlaces de símbolos ELF. Para dicho símbolo, el enlazador dinámico se asegurará de que en todo el proceso solo haya un símbolo con este nombre y tipo en uso.

entonces vemos que hay una extensión ELF dedicada para esto.

Pre-C ++ 17: extern const

Antes de C ++ 17, y en C, podemos lograr un efecto muy similar con un extern const, lo que conducirá a que se use una única ubicación de memoria.

Los inconvenientes inlineson:

  • no es posible hacer la variable constexprcon esta técnica, solo inlinepermite eso: ¿Cómo declarar constexpr extern?
  • es menos elegante ya que debe declarar y definir la variable por separado en el encabezado y el archivo 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 aguas arriba .

Pre-C ++ 17 encabezado solo alternativas

Estos no son tan buenos como la externsolución, pero funcionan y solo ocupan una única ubicación de memoria:

Una constexprfunción, porque constexprimplicainline y inline permite (fuerza) que la definición aparezca en cada unidad de traducción :

constexpr int shared_inline_constexpr() { return 42; }

y apuesto a que cualquier compilador decente alineará la llamada.

También puede usar una variable entera consto constexprestática como en:

#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;
}

pero no puede hacer cosas como tomar su dirección, o de lo contrario se usará odr, vea también: https://en.cppreference.com/w/cpp/language/static "Miembros estáticos constantes" y Definiendo datos estáticos constexpr miembros

C

En C la situación es la misma que C ++ pre C ++ 17, he subido un ejemplo en: ¿Qué significa "estático" en C?

La única diferencia es que en C ++, constimplica staticpara globales, pero no en C: C ++ semántica de 'static const' vs 'const`

¿Alguna forma de alinearlo completamente?

TODO: ¿hay alguna forma de alinear completamente la variable, sin usar memoria alguna?

Muy parecido a lo que hace el preprocesador.

Esto requeriría de alguna manera:

  • prohibir o detectar si se toma la dirección de la variable
  • agregue esa información a los archivos de objetos ELF y deje que LTO la optimice

Relacionado:

Probado en Ubuntu 18.10, GCC 8.2.0.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fuente
2
inlineno tiene casi nada que ver con la alineación, ni para funciones ni para variables, a pesar de la palabra misma. inlineno le dice al compilador que inserte nada en línea. Le dice al enlazador que se asegure de que solo haya una definición, que tradicionalmente ha sido el trabajo del programador. Entonces, "¿Alguna forma de alinearlo completamente?" es al menos una pregunta completamente no relacionada.
no es un usuario