É seguro usar uma variável estática de escopo de namespace (ou seja, ligação interna) como parâmetro padrão para uma função declarada em um cabeçalho? E se sim, é garantido que ao fazer uma chamada padrão em uma determinada unidade de tradução, o valor definido naquela unidade de tradução seja usado como padrão?
Em código:
biblioteca.h:
#pragma once
#ifdef USE_ALTERNATE_DEFAULT
static const int defaultValue = 42;
#else
static const int defaultValue = 314;
#endif
void printValue(int value = defaultValue);
biblioteca.cpp:
#include "lib.h"
#include <print>
void printValue(const int value) {
std::println("Value: {}", value);
}
a.cpp:
#include "lib.h"
void foo() {
printValue(); // '314'
printValue(0); // '0'
}
b.cpp:
#define USE_ALTERNATE_DEFAULT
#include "lib.h"
void bar() {
printValue(); // '42'
printValue(1); // '1'
}
O código acima está bem formado? E é garantido que cada uma das chamadas para printValue
resulte no valor no comentário correspondente sendo impresso?
Tenha em mente que lib.cpp, a.cpp e b.cpp podem fazer parte de diferentes bibliotecas/binários compartilhados.
O objetivo aqui é tornar o comportamento padrão de uma função personalizável em um nÃvel por TU alterando as diretivas do pré-processador.
Contexto para a pergunta
Dito isto, a solução parece um tanto complicada e este pode ser um problema XY.
Aqui está uma descrição mais completa do que estou tentando fazer:
- Estou usando algo semelhante para
bin2c
gerar vários arquivos de cabeçalho que definem várias strings longas diferentes. - Tenho uma biblioteca vinculada estaticamente
lib
que precisa executar alguma operação nessas strings geradas automaticamente ou retornar à operação em uma string padrão caso nenhum cabeçalho desse tipo esteja disponÃvel. - Estou compilando várias versões diferentes de vários binários diferentes, cada um com diferentes arquivos de cabeçalho gerados automaticamente, cada um vinculado à lib.
Então lib.h
pode ser algo como:
#ifdef GENERATED_HEADER
// static const char* kMyString defined in the generated header
#include GENERATED_HEADER
#else
// Define fall-back default.
static const char* kMyString = "default";
#endif
void doStuff(int someArg, const char* str = kMyString);
E claro lib.cpp
tem a definição de doStuff
. Suponha que esta seja uma função complexa.
Então my_app1.cpp
:
#include "lib.h"
int main() {
int someArg = foo();
doStuff(someArg);
}
E my_app2.cpp
:
#include "lib.h"
int main() {
int someArg = bar();
doStuff(someArg);
}
Então:
- Compilar
lib.cpp
emlib.a
. - Gerar
Doc1.h
usandodoc1.txt
bin2c-ish. - Gerar
Doc2.h
usandodoc2.txt
bin2c-ish. - Gerar
Doc3.h
usandodoc3.txt
bin2c-ish. - Compilar
my_app1.cpp
emy_app1_default.a
vincular comlib.a
intomy_app1_default
. - Compilar
my_app1.cpp
com-DGENERATED_HEADER=\"Doc1.h\"
intomy_app2_doc1.a
e vincular comlib.a
intomy_app1_doc1
. - Compilar
my_app1.cpp
com-DGENERATED_HEADER=\"Doc2.h\"
intomy_app1_doc2.a
e vincular comlib.a
intomy_app1_doc2
. - Compilar
my_app2.cpp
emy_app2_default.a
vincular comlib.a
intomy_app2_default
. - Compilar
my_app2.cpp
com-DGENERATED_HEADER=\"Doc3.h\"
intomy_app2_doc3.a
e vincular comlib.a
intomy_app2_doc3
.
O ponto é que o comportamento de lib
pode ser personalizado em um nÃvel por binário com base no ambiente de construção, enquanto ainda mantém uma única implementação de lib
. E, além disso, lib
pode ser usado em vários códigos-fonte de aplicativos diferentes com código boilerplate mÃnimo.
Claro, se a abordagem do argumento padrão for malformada, aproximadamente a mesma coisa ainda pode ser alcançada simplesmente passando explicitamente a variável estática para a função. Mas isso exigiria um pouco mais de boilerplate no local da chamada, o que estou tentando evitar. Daà a pergunta acima :)
TL;DR É uma violação de ODR. Sempre que você acha que pode enganar o compilador para fazer coisas diferentes para o mesmo código-fonte em TUs diferentes trocando furtivamente seus componentes, é uma violação de ODR.
De basic.def.odr/1
básico.def.odr/14
Os argumentos padrão aparecem em várias TUs.
E é o que acontece.
Não , porque cada TU tem o seu próprio
defaultValue
. Então olhamos para as exceçõesdefaultValue
satisfaz todos os pontos, exceto o último, o que definitivamente não acontece.