É seguro vincular objetos C ++ 17, C ++ 14 e C ++ 11

97

Suponha que eu tenha três objetos compilados, todos produzidos pelo mesmo compilador / versão :

  1. A foi compilado com o padrão C ++ 11
  2. B foi compilado com o padrão C ++ 14
  3. C foi compilado com o padrão C ++ 17

Para simplificar, vamos supor que todos os cabeçalhos foram escritos em C ++ 11, usando apenas construções cuja semântica não mudou entre as três versões padrão e, portanto, quaisquer interdependências foram expressas corretamente com a inclusão do cabeçalho e o compilador não fez objeções.

Quais combinações desses objetos é e não é seguro vincular em um único binário? Por quê?


EDITAR: respostas cobrindo os principais compiladores (por exemplo, gcc, clang, vs ++) são bem-vindas

ricab
fonte
6
Não é uma pergunta da escola / entrevista. A questão surge de um caso particular: estou trabalhando em um projeto que depende de uma biblioteca de código aberto. Eu construo esta biblioteca a partir do código-fonte, mas seu sistema de construção aceita apenas um sinalizador para escolher entre a construção C ++ 03 / C ++ 11. O compilador que uso oferece suporte a outros padrões, porém, e estou pensando em atualizar meu próprio projeto para C ++ 17. Não tenho certeza se é uma decisão segura. Pode haver uma interrupção no ABI ou alguma outra forma em que a abordagem não seja aconselhável? Não encontrei uma resposta clara e decidi postar uma pergunta sobre o caso geral.
ricab
6
Isso depende inteiramente do compilador. Não há nada nas especificações formais do C ++ que governe essa situação. Há também uma pequena possibilidade de que o código que foi escrito para os padrões C ++ 03 ou C + 11 tenha alguns problemas nos níveis C ++ 14 e C ++ 17. Com conhecimento e experiência suficientes (e código bem escrito para começar), deve ser possível corrigir qualquer um desses problemas. Se você não está, entretanto, muito familiarizado com os padrões C ++ mais novos, é melhor se ater ao que o sistema de compilação suporta e é testado para funcionar.
Sam Varshavchik
9
@Someprogrammerdude: É uma pergunta extremamente interessante. Eu gostaria de ter uma resposta. Tudo o que sei é que libstdc ++ via RHEL devtoolset é compatível com as versões anteriores por design, vinculando estaticamente as coisas mais novas e deixando as coisas mais antigas para resolver dinamicamente em tempo de execução usando a libstdc ++ "nativa" da distro. Mas isso não responde à pergunta.
Lightness Races in Orbit
3
@nm: ... o que é principalmente o caso ... praticamente todo mundo que distribui bibliotecas C ++ independentes de distribuição o faz (1) na forma de biblioteca dinâmica e (2) sem contêineres de biblioteca padrão C ++ nos limites da interface. As bibliotecas que vêm de uma distribuição Linux têm mais facilidade, pois todas são construídas com o mesmo compilador, a mesma biblioteca padrão e praticamente o mesmo conjunto padrão de sinalizadores.
Matteo Italia
3
Apenas para esclarecer o comentário anterior de @MatteoItalia "e ao alternar do modo C ++ 03 para C ++ 11 (std :: string em particular)." Isso não é verdade, a std::stringimplementação ativa em libstdc ++ é independente do -stdmodo usado . Esta é uma propriedade importante, justamente para dar suporte a situações como os OP's. Você pode usar o novo std::stringno código C ++ 03, e você pode usar o antigo std::stringno código C ++ 11 (consulte o link no comentário posterior de Matteo).
Jonathan Wakely,

Respostas:

116

Quais combinações desses objetos é e não é seguro vincular em um único binário? Por quê?

Para o GCC , é seguro vincular qualquer combinação dos objetos A, B e C. Se todos forem construídos com a mesma versão, então são compatíveis com ABI, a versão padrão (ou seja, a -stdopção) não faz nenhuma diferença.

Por quê? Porque essa é uma propriedade importante de nossa implementação que trabalhamos muito para garantir.

Onde você tem problemas é vincular objetos compilados com versões diferentes do GCC e usar recursos instáveis ​​de um novo padrão C ++ antes que o suporte do GCC para esse padrão seja concluído. Por exemplo, se você compilar um objeto usando GCC 4.9 -std=c++11e outro objeto com GCC 5 e -std=c++11você terá problemas. O suporte a C ++ 11 era experimental no GCC 4.x, portanto, havia alterações incompatíveis entre o GCC 4.9 e 5 versões dos recursos do C ++ 11. Da mesma forma, se você compilar um objeto com GCC 7 e -std=c++17outro objeto com GCC 8 e -std=c++17terá problemas, porque o suporte a C ++ 17 no GCC 7 e 8 ainda é experimental e em evolução.

Por outro lado, qualquer combinação dos seguintes objetos funcionará (embora veja a nota abaixo sobre a libstdc++.soversão):

  • objeto D compilado com GCC 4.9 e -std=c++03
  • objeto E compilado com GCC 5 e -std=c++11
  • objeto F compilado com GCC 7 e -std=c++17

Isso ocorre porque o suporte ao C ++ 03 é estável em todas as três versões do compilador usadas e, portanto, os componentes do C ++ 03 são compatíveis entre todos os objetos. O suporte a C ++ 11 é estável desde GCC 5, mas o objeto D não usa nenhum recurso do C ++ 11, e os objetos E e F usam versões em que o suporte a C ++ 11 é estável. O suporte a C ++ 17 não é estável em nenhuma das versões do compilador usadas, mas apenas o objeto F usa recursos do C ++ 17 e, portanto, não há problema de compatibilidade com os outros dois objetos (os únicos recursos que eles compartilham vêm do C ++ 03 ou C ++ 11, e as versões usadas tornam essas partes OK). Se mais tarde você quisesse compilar um quarto objeto, G, usando GCC 8 e -std=c++17então precisaria recompilar F com a mesma versão (ou não vincular a F) porque os símbolos do C ++ 17 em F e G são incompatíveis.

A única ressalva para a compatibilidade descrita acima entre D, E e F é que seu programa deve usar a libstdc++.sobiblioteca compartilhada do GCC 7 (ou posterior). Como o objeto F foi compilado com GCC 7, você precisa usar a biblioteca compartilhada dessa versão, porque compilar qualquer parte do programa com GCC 7 pode introduzir dependências em símbolos que não estão presentes no libstdc++.soGCC 4.9 ou GCC 5. Da mesma forma, se você vinculou ao objeto G, construído com GCC 8, você precisaria usar o libstdc++.sodo GCC 8 para garantir que todos os símbolos necessários para G sejam encontrados. A regra simples é garantir que a biblioteca compartilhada que o programa usa em tempo de execução seja pelo menos tão nova quanto a versão usada para compilar qualquer um dos objetos.

Outra advertência ao usar o GCC, já mencionada nos comentários sobre sua pergunta, é que, desde o GCC 5, existem duas implementaçõesstd::string disponíveis em libstdc ++. As duas implementações não são compatíveis com link (eles têm nomes mutilados diferentes, então não podem ser ligados entre si), mas podem coexistir no mesmo binário (eles têm nomes mutilados diferentes, então não entre em conflito se um objeto usar std::stringe o outros usos std::__cxx11::string). Se seus objetos usam std::string, geralmente eles devem ser compilados com a mesma implementação de string. Compile com -D_GLIBCXX_USE_CXX11_ABI=0para selecionar a gcc4-compatibleimplementação original , ou -D_GLIBCXX_USE_CXX11_ABI=1para selecionar a nova cxx11implementação (não se deixe enganar pelo nome, ele também pode ser usado em C ++ 03, é chamadocxx11porque está em conformidade com os requisitos do C ++ 11). Qual implementação é o padrão depende de como o GCC foi configurado, mas o padrão sempre pode ser substituído em tempo de compilação com a macro.

Jonathan Wakely
fonte
"porque a compilação de qualquer parte do programa com o GCC 7 pode introduzir dependências nos símbolos que estão presentes no libstdc ++. então do GCC 4.9 ou GCC 5" você quis dizer que NÃO estão presentes no GCC 4.9 ou GCC 5, certo? Isso também se aplica a links estáticos? Obrigado pela informação sobre compatibilidade entre as versões do compilador.
Hadi Brais
1
Acabei de perceber uma falha enorme em oferecer uma recompensa por essa questão. 😂
Lightness Races in Orbit
4
@ricab tenho 90% de certeza de que a resposta é a mesma para Clang / libc ++, mas não tenho ideia sobre MSVC.
Jonathan Wakely
1
Esta resposta é estelar. Está documentado em algum lugar que 5.0+ é estável para 14/11?
Barry
1
Não muito claramente ou em um só lugar. gcc.gnu.org/gcc-5/changes.html#libstdcxx e gcc.gnu.org/onlinedocs/libstdc++/manual/api.html#api.rel_51 declaram que o suporte da biblioteca para C ++ 11 está completo (a linguagem o suporte foi concluído anteriormente, mas ainda "experimental"). O suporte à biblioteca C ++ 14 ainda está listado como experimental até o 6.1, mas acho que na prática nada mudou entre 5.xe 6.x que afete o ABI.
Jonathan Wakely
16

A resposta tem duas partes. Compatibilidade no nível do compilador e compatibilidade no nível do vinculador. Vamos começar com o primeiro.

vamos supor que todos os cabeçalhos foram escritos em C ++ 11

Usar o mesmo compilador significa que o mesmo cabeçalho da biblioteca padrão e arquivos de origem (os primeiros associados ao compilador) serão usados ​​independentemente do padrão C ++ de destino. Portanto, os arquivos de cabeçalho da biblioteca padrão são escritos para serem compatíveis com todas as versões C ++ suportadas pelo compilador.

Dito isso, se as opções do compilador usadas para compilar uma unidade de tradução especificam um determinado padrão C ++, quaisquer recursos que estão disponíveis apenas em padrões mais novos não devem estar acessíveis. Isso é feito usando a __cplusplusdiretiva. Veja o arquivo de origem do vetor para um exemplo interessante de como ele é usado. Da mesma forma, o compilador rejeitará quaisquer recursos sintáticos oferecidos pelas versões mais recentes do padrão.

Tudo isso significa que sua suposição só pode ser aplicada aos arquivos de cabeçalho que você escreveu. Esses arquivos de cabeçalho podem causar incompatibilidades quando incluídos em diferentes unidades de tradução direcionadas a diferentes padrões C ++. Isso é discutido no Anexo C do padrão C ++. Existem 4 cláusulas, discutirei apenas a primeira e mencionarei brevemente o resto.

C.3.1 Cláusula 2: convenções lexicais

As aspas simples delimitam um literal de caractere em C ++ 11, enquanto são separadores de dígitos em C ++ 14 e C ++ 17. Suponha que você tenha a seguinte definição de macro em um dos arquivos de cabeçalho C ++ 11 puros:

#define M(x, ...) __VA_ARGS__

// Maybe defined as a field in a template or a type.
int x[2] = { M(1'2,3'4) };

Considere duas unidades de tradução que incluem o arquivo de cabeçalho, mas visam C ++ 11 e C ++ 14, respectivamente. Ao direcionar o C ++ 11, a vírgula entre aspas não é considerada um separador de parâmetro; existe apenas um parâmetro. Portanto, o código seria equivalente a:

int x[2] = { 0 }; // C++11

Por outro lado, ao direcionar o C ++ 14, as aspas simples são interpretadas como separadores de dígitos. Portanto, o código seria equivalente a:

int x[2] = { 34, 0 }; // C++14 and C++17

O ponto aqui é que usar aspas simples em um dos arquivos de cabeçalho C ++ 11 puros pode resultar em erros surpreendentes nas unidades de tradução voltadas para C ++ 14/17. Portanto, mesmo se um arquivo de cabeçalho for escrito em C ++ 11, ele deve ser escrito com cuidado para garantir que seja compatível com as versões posteriores do padrão. A __cplusplusdiretiva pode ser útil aqui.

As outras três cláusulas da norma incluem:

C.3.2 Cláusula 3: conceitos básicos

Alteração : Novo desalocador usual (não posicionamento)

Justificativa : necessária para desalocação de tamanho.

Efeito no recurso original : o código C ++ 2011 válido pode declarar uma função de alocação de posicionamento global e função de desalocação da seguinte forma:

void operator new(std::size_t, std::size_t); 
void operator delete(void*, std::size_t) noexcept;

Nesta Norma Internacional, entretanto, a declaração de exclusão de operador pode corresponder a uma exclusão de operador usual (não posicionamento) predefinida (3.7.4). Se for assim, o programa está mal formado, como era para funções de alocação de membros de classe e funções de desalocação (5.3.4).

C.3.3 Cláusula 7: declarações

Alteração : as funções de membro não estáticas constexpr não são funções de membro const implicitamente.

Justificativa : Necessário para permitir que funções-membro constexpr alterem o objeto.

Efeito no recurso original : O código C ++ 2011 válido pode falhar ao compilar nesta Norma.

Por exemplo, o código a seguir é válido em C ++ 2011, mas inválido neste Padrão Internacional porque declara a mesma função de membro duas vezes com tipos de retorno diferentes:

struct S {
constexpr const int &f();
int &f();
};

C.3.4 Cláusula 27: biblioteca de entrada / saída

Alteração : get não está definido.

Justificativa : O uso de get é considerado perigoso.

Efeito no recurso original : o código C ++ 2011 válido que usa a função gets pode falhar ao compilar neste Padrão Internacional.

As incompatibilidades potenciais entre C ++ 14 e C ++ 17 são discutidas em C.4. Como todos os arquivos de cabeçalho não padrão são escritos em C ++ 11 (conforme especificado na pergunta), esses problemas não ocorrerão, portanto, não os mencionarei aqui.

Agora vou discutir a compatibilidade no nível do vinculador. Em geral, os motivos potenciais para incompatibilidades incluem o seguinte:

Se o formato do arquivo de objeto resultante depender do padrão C ++ de destino, o vinculador deve ser capaz de vincular os diferentes arquivos de objeto. No GCC, LLVM e VC ++, felizmente esse não é o caso. Ou seja, o formato dos arquivos de objetos é o mesmo, independentemente do padrão de destino, embora seja altamente dependente do próprio compilador. Na verdade, nenhum dos vinculadores de GCC, LLVM e VC ++ requer conhecimento sobre o padrão C ++ de destino. Isso também significa que podemos vincular arquivos-objeto que já estão compilados (vinculando estaticamente o tempo de execução).

Se a rotina de inicialização do programa (a função que chama main) for diferente para diferentes padrões C ++ e as diferentes rotinas não forem compatíveis entre si, então não seria possível vincular os arquivos-objeto. No GCC, LLVM e VC ++, felizmente esse não é o caso. Além disso, a assinatura da mainfunção (e as restrições que se aplicam a ela, consulte a Seção 3.6 do padrão) é a mesma em todos os padrões C ++, portanto, não importa em qual unidade de tradução ela existe.

Em geral, o WPO pode não funcionar bem com arquivos-objeto compilados usando padrões C ++ diferentes. Isso depende exatamente de quais estágios do compilador exigem conhecimento do padrão de destino e quais não, e do impacto que isso tem nas otimizações entre procedimentos que cruzam arquivos de objeto. Felizmente, GCC, LLVM e VC ++ são bem projetados e não têm esse problema (não que eu saiba).

Portanto, GCC, LLVM e VC ++ foram projetados para permitir compatibilidade binária em diferentes versões do padrão C ++. Este não é realmente um requisito do padrão em si.

A propósito, embora o compilador VC ++ ofereça a opção std , que permite direcionar uma versão específica do padrão C ++, ele não oferece suporte ao direcionamento C ++ 11. A versão mínima que pode ser especificada é C ++ 14, que é o padrão a partir da atualização 3 do Visual C ++ 2013 para compilar diferentes unidades de tradução que visam diferentes versões do padrão C ++, o que no mínimo quebraria o WPO.

CAVEAT: Minha resposta pode não ser completa ou muito precisa.

Hadi Brais
fonte
A questão realmente se referia a vinculação em vez de compilação. Reconheço (graças a este comentário ) que talvez não estivesse claro e o editei para deixar claro que todos os cabeçalhos incluídos têm a mesma interpretação em todos os três padrões.
Ricab
@ricab A resposta cobre tanto a compilação quanto a vinculação. Achei que você estivesse perguntando sobre os dois.
Hadi Brais
1
Na verdade, mas acho que a resposta é muito longa e confusa, especialmente até "Agora vou discutir a compatibilidade no nível do vinculador". Você poderia substituir tudo acima por algo como se os cabeçalhos incluídos não pudessem ser postulados como tendo o mesmo significado em C ++ 11 e C ++ 14/17, então não é seguro incluí-los em primeiro lugar . Para a parte restante, você tem uma fonte mostrando que esses três pontos são os únicos motivos potenciais para incompatibilidade? Obrigado pela resposta em qualquer caso, ainda estou votando
ricab
@ricab não posso dizer com certeza. É por isso que adicionei a advertência no final da resposta. Qualquer outra pessoa é bem-vinda para expandir a resposta para torná-la mais precisa ou completa, caso eu tenha perdido algo.
Hadi Brais
Isso me confunde: "Usar o mesmo compilador significa que o mesmo cabeçalho de biblioteca padrão e arquivos de origem (...) serão usados". Como pode ser esse o caso? Se eu tiver um código antigo compilado com gcc5, os 'arquivos do compilador' que pertenciam a essa versão não podem ser à prova de futuro. Para o código-fonte compilado em momentos (descontroladamente) diferentes com diferentes versões do compilador, podemos ter certeza de que o cabeçalho da biblioteca e os arquivos-fonte são diferentes. Com sua regra de que eles devem ser iguais, você deve recompilar o código-fonte antigo com gcc5, ... e certificar-se de que todos usam os (mesmos) 'arquivos de compilador' mais recentes.
user2943111
2

Os novos padrões C ++ vêm em duas partes: recursos de linguagem e componentes de biblioteca padrão.

Como você quer dizer com novo padrão , mudanças no idioma em si (por exemplo, variado para) quase não há problema (às vezes existem conflitos em cabeçalhos de bibliotecas de terceiros com recursos de linguagem padrão mais recentes).

Mas a biblioteca padrão ...

Cada versão do compilador vem com uma implementação da biblioteca padrão C ++ (libstdc ++ com gcc, libc ++ com clang, biblioteca padrão MS C ++ com VC ++, ...) e exatamente uma implementação, não muitas implementações para cada versão padrão. Além disso, em alguns casos, você pode usar outra implementação da biblioteca padrão além do compilador fornecido. Você deve se preocupar em vincular uma implementação de biblioteca padrão mais antiga a uma mais recente.

O conflito que pode ocorrer entre bibliotecas de terceiros e seu código é a biblioteca padrão (e outras bibliotecas) que se vincula a essas bibliotecas de terceiros.

E. Vakili
fonte
"Cada versão do compilador vem com uma implementação de STL" Não, não
Lightness Races in Orbit
@LightnessRacesinOrbit Você quer dizer que não há relação entre, por exemplo, libstdc ++ e gcc?
E. Vakili
8
Não, quero dizer que o STL está efetivamente obsoleto há pouco mais de vinte anos. Você quis dizer a Biblioteca Padrão C ++. Quanto ao resto da resposta, você pode fornecer algumas referências / evidências para apoiar sua reclamação? Acho que para uma pergunta como essa é importante.
Lightness Races in Orbit
3
Desculpe, não, não está claro no texto. Você fez algumas afirmações interessantes, mas ainda não as apoiou com nenhuma evidência.
Corridas de leveza em órbita