Como as variáveis ​​embutidas funcionam?

124

Na reunião de padrões ISO C ++ do Oulu de 2016, uma proposta chamada Variáveis ​​em linha foi votada no C ++ 17 pelo comitê de padrões.

Em termos leigos, quais são as variáveis ​​embutidas, como elas funcionam e para que servem? Como as variáveis ​​embutidas devem ser declaradas, definidas e usadas?

jotik
fonte
@ jotik Acho que a operação equivalente estaria substituindo qualquer ocorrência da variável por seu valor. Normalmente, este só é válido se a variável é const.
melpomene
5
Essa não é a única coisa que a inlinepalavra - chave faz para funções. A inlinepalavra-chave, quando aplicada a funções, tem outro efeito crucial, que se traduz diretamente em variáveis. Uma inlinefunção, presumivelmente declarada em um arquivo de cabeçalho, não resultará em erros de "símbolo duplicado" no momento do link, mesmo que o cabeçalho seja #included por várias unidades de conversão. A inlinepalavra-chave, quando aplicada a variáveis, terá o mesmo resultado exato. O fim.
Sam Varshavchik
4
^ No sentido de 'substituir qualquer chamada para esta função por uma cópia no local', inlineé apenas uma solicitação fraca e não vinculativa ao otimizador. Os compiladores são livres para não incorporar as funções solicitadas e / ou incorporar as que você não anotou. Em vez disso, o objetivo real da inlinepalavra-chave é contornar erros de definição múltipla.
Underscore_d

Respostas:

121

A primeira frase da proposta:

O inlineespecificador pode ser aplicado a variáveis, bem como para as funções.

O efeito garantido de inlineaplicado a uma função é permitir que a função seja definida de forma idêntica, com ligação externa, em várias unidades de conversão. Para a prática, isso significa definir a função em um cabeçalho, que pode ser incluído em várias unidades de conversão. A proposta estende essa possibilidade a variáveis.

Portanto, em termos práticos, a proposta (agora aceita) permite que você use a inlinepalavra-chave para definir uma constvariável de escopo de espaço para nome de ligação externa ou qualquer staticmembro de dados de classe em um arquivo de cabeçalho, para que as várias definições resultantes quando esse cabeçalho seja incluído no várias unidades de tradução estão bem com o vinculador - apenas escolhe uma delas.

Até o C ++ 14, inclusive, o mecanismo interno para isso existia, a fim de suportar staticvariáveis ​​nos modelos de classe, mas não havia uma maneira conveniente de usar esse mecanismo. Era preciso recorrer a truques 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`.

A partir do C ++ 17 em diante, acredito que se possa escrever apenas

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

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

... em um arquivo de cabeçalho.

A proposta inclui a redação

Um membro de dados em linha estática pode ser definido na definição de classe e pode s pecify uma cinta-ou-igual inicializador. Se o membro for declarado com o constexprespecificador, ele poderá ser declarado novamente no escopo do espaço para nome sem inicializador (esse uso foi descontinuado; consulte ‌ DX). As declarações de outros membros de dados estáticos não devem especificar um colchete ou equalizador de inicialização

… O que permite que o acima seja mais simplificado para apenas

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

… Como observado pelo TC em um comentário a esta resposta.

Além disso, o  ​constexprespecificador implica  inline em membros de dados estáticos e também em funções.


Notas:
¹ Para uma função inlinetambém tem um efeito de dica sobre otimização, que o compilador deve preferir substituir as chamadas dessa função pela substituição direta do código de máquina da função. Essa dica pode ser ignorada.

Felicidades e hth. - Alf
fonte
2
Além disso, a restrição const se aplica apenas a variáveis ​​de escopo de namespace. Os de escopo de classe (como Kath::hi) não precisam ser const.
TC
4
Relatórios mais recentes indicam que a constrestrição foi totalmente descartada.
TC
2
@ Nick: Como Richard Smith (o atual "editor de projetos" do comitê C ++) é um dos dois autores e como ele é "o proprietário do código do front-end do Clang C ++", adivinhou Clang. E a construção compilada com o clang 3.9.0 no Godbolt . Ele avisa que variáveis ​​em linha são uma extensão C ++ 1z. Não encontrei nenhuma maneira de compartilhar as opções e opções de origem e compilador; portanto, o link é apenas para o site em geral, desculpe.
Saúde e hth. - Alf
1
por que necessidade de palavra-chave embutida dentro da declaração de classe / estrutura? Por que não permitir simplesmente static std::string const hi = "Zzzzz...";?
Sasha.sochka
2
@EmilianCioca: Não, você entraria em conflito com o fiasco da ordem de inicialização estática . Um singleton é essencialmente um dispositivo para evitar isso.
Saúde e hth. #
15

As variáveis ​​embutidas são muito semelhantes às funções embutidas. Ele sinaliza ao vinculador que apenas uma instância da variável deve existir, mesmo que a variável seja vista em várias unidades de compilação. O vinculador precisa garantir que não sejam criadas mais cópias.

Variáveis ​​em linha podem ser usadas para definir globais em bibliotecas de cabeçalho apenas. Antes do C ++ 17, eles tinham que usar soluções alternativas (funções embutidas ou hacks de modelo).

Por exemplo, uma solução alternativa é usar o singleton de Meyer com uma função embutida:

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

Existem algumas desvantagens nessa abordagem, principalmente em termos de desempenho. Essa sobrecarga pode ser evitada por soluções de modelos, mas é fácil errar.

Com variáveis ​​embutidas, você pode declará-lo diretamente (sem obter um erro do vinculador de várias definições):

inline T global;

Além das bibliotecas de cabeçalho apenas, existem outros casos em que variáveis ​​em linha podem ajudar. Nir Friedman aborda esse tópico em sua palestra no CppCon: O que os desenvolvedores de C ++ devem saber sobre os globais (e o vinculador) . A parte sobre variáveis ​​embutidas e as soluções alternativas começa aos 18m9s .

Para encurtar a história, se você precisar declarar variáveis ​​globais que são compartilhadas entre unidades de compilação, declará-las como variáveis ​​embutidas no arquivo de cabeçalho é simples e evita os problemas com soluções alternativas anteriores ao C ++ 17.

(Ainda existem casos de uso para o singleton do Meyer, por exemplo, se você desejar explicitamente uma inicialização lenta.)

Philipp Claßen
fonte
11

Exemplo mínimo executável

Esse incrível recurso do C ++ 17 nos permite:

  • use convenientemente apenas um único endereço de memória para cada constante
  • armazene-o como constexpr: Como declarar constexpr extern?
  • faça isso em uma única linha a partir de um cabeçalho

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

Compile e execute:

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

Consulte também: Como as variáveis ​​embutidas funcionam?

Padrão C ++ em variáveis ​​embutidas

O padrão C ++ garante que os endereços serão os mesmos. Rascunho padrão do C ++ 17 N4659 10.1.6 "O especificador em linha":

6 Uma função em linha ou variável com ligação externa deve ter o mesmo endereço em todas as unidades de tradução.

A cppreference https://en.cppreference.com/w/cpp/language/inline explica que, se staticnão for fornecida, ela terá ligação externa.

Implementação de variável em linha do GCC

Podemos observar como é implementado com:

nm main.o notmain.o

que contém:

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

e man nmdiz sobre u:

"u" O símbolo é um símbolo global exclusivo. Esta é uma extensão GNU para o conjunto padrão de ligações de símbolos ELF. Para esse símbolo, o vinculador dinâmico garantirá que, durante todo o processo, exista apenas um símbolo com esse nome e digite em uso.

então vemos que há uma extensão ELF dedicada para isso.

Pré-C ++ 17: extern const

Antes de C ++ 17 e em C, podemos obter um efeito muito semelhante a um extern const, o que levará a um único local de memória sendo usado.

As desvantagens inlinesão:

  • não é possível fazer a variável constexprcom esta técnica, apenas inlinepermite que: Como declarar constexpr extern?
  • é menos elegante, pois é necessário declarar e definir a variável separadamente no cabeçalho e no arquivo 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 upstream .

Apenas alternativas de cabeçalho pré-C ++ 17

Eles não são tão bons quanto a externsolução, mas funcionam e ocupam apenas um único local de memória:

Uma constexprfunção, porque constexprimplicainline e inline permite (força) a definição para aparecer em todas as unidades de tradução :

constexpr int shared_inline_constexpr() { return 42; }

e aposto que qualquer compilador decente atenderá a chamada.

Você também pode usar uma variável inteira estática constou constexprcomo em:

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

mas você não pode fazer coisas como usar o endereço, ou ele se torna usado por odr, consulte também: https://en.cppreference.com/w/cpp/language/static "Membros estáticos constantes" e Definição de dados estáticos constexpr membros

C

Em C, a situação é a mesma que C ++ pré C ++ 17, enviei um exemplo em: O que significa "estático" em C?

A única diferença é que, em C ++, constimplica staticem globais, mas não na semântica de C: C ++ de `const const` vs` const`

Alguma maneira de incorporá-lo totalmente?

TODO: existe alguma maneira de alinhar totalmente a variável, sem usar nenhuma memória?

Muito parecido com o que o pré-processador faz.

Isso exigiria de alguma forma:

  • proibir ou detectar se o endereço da variável é usado
  • adicione essas informações aos arquivos de objeto ELF e deixe o LTO otimizá-las

Palavras-chave:

Testado no Ubuntu 18.10, GCC 8.2.0.

Ciro Santilli adicionou uma nova foto
fonte
2
inlinenão tem quase nada a ver com inlining, nem para funções nem para variáveis, apesar da própria palavra. inlinenão diz ao compilador para incorporar nada. Ele informa ao vinculador para garantir que haja apenas uma definição, que tem sido tradicionalmente o trabalho do programador. Então, "Alguma maneira de incorporá-lo totalmente?" é pelo menos uma questão completamente não relacionada.
não usuário