Is it safe to use a namespace-scope static variable (i.e., internal linkage) as the default parameter for a function declared in a header? And if so, is it guaranteed that when making a defaulted call in a certain translation unit, the value defined in that translation unit is used as the default?
In code:
lib.h:
#pragma once
#ifdef USE_ALTERNATE_DEFAULT
static const int defaultValue = 42;
#else
static const int defaultValue = 314;
#endif
void printValue(int value = defaultValue);
lib.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'
}
Is the code above well-formed? And is it guaranteed that each of the calls to printValue
result in the value in the corresponding comment being printed?
Keep in mind that lib.cpp, a.cpp, and b.cpp could be part of different shared libraries / binaries.
The goal here is to make the default behavior of a function customizable on a per-TU level by changing the preprocessor directives.
Having said the above, the solution seems rather convoluted and this might be an XY problem.
Here is a more complete description of what I'm trying to do:
bin2c
to generate various header files that define various different long strings.lib
that needs to do some operation on these auto-generated strings, or fall back to operating on a default string if no such header is available.So lib.h
might look something like:
#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);
And of course lib.cpp
has the definition of doStuff
. Assume this is a complex function.
Then my_app1.cpp
:
#include "lib.h"
int main() {
int someArg = foo();
doStuff(someArg);
}
And my_app2.cpp
:
#include "lib.h"
int main() {
int someArg = bar();
doStuff(someArg);
}
Then:
lib.cpp
into lib.a
.Doc1.h
from doc1.txt
using bin2c-ish.Doc2.h
from doc2.txt
using bin2c-ish.Doc3.h
from doc3.txt
using bin2c-ish.my_app1.cpp
into my_app1_default.a
and link with lib.a
into my_app1_default
.my_app1.cpp
with -DGENERATED_HEADER=\"Doc1.h\"
into my_app2_doc1.a
and link with lib.a
into my_app1_doc1
.my_app1.cpp
with -DGENERATED_HEADER=\"Doc2.h\"
into my_app1_doc2.a
and link with lib.a
into my_app1_doc2
.my_app2.cpp
into my_app2_default.a
and link with lib.a
into my_app2_default
.my_app2.cpp
with -DGENERATED_HEADER=\"Doc3.h\"
into my_app2_doc3.a
and link with lib.a
into my_app2_doc3
.The point is that the behavior of lib
can be customized on a per-binary level based on the build environment, while still maintaining a single implementation of lib
. And moreover, lib
can be used ini multiple different applications' source code with minimal boilerplate code.
Of course, if the default-argument approach is ill-formed, roughly the same thing can still be achieved by simply explicitly passing the static variable to the function. But that would require a bit more boilerplate at the call-site, which I'm trying to avoid. Hence the question above :)
TL;DR It's an ODR violation. Whenever you think you can trick the compiler into doing different things for the same source code in different TUs by sneakily swapping out its components, it's an ODR violation.
From basic.def.odr/1
Each of the following is termed a definable item: [...]
- a default argument for a parameter (for a function in a given scope)
For any definable item D with definitions in multiple translation units, [...]
- if the definitions in different translation units do not satisfy the following requirements,
the program is ill-formed [...]
The default arguments do appear in multiple TUs.
Given such an item, for all definitions of D, [...] the following requirements shall be satisfied.
- Each such definition shall consist of the same sequence of tokens
Which it does.
- In each such definition, corresponding names, looked up according to [basic.lookup], shall refer to the same entity, [...] except that a name can refer to
It does not, because each TU gets its own defaultValue
. So we look at the exceptions
- a non-volatile const object with internal or no linkage if the object [...]
- has the same literal type in all definitions of D,
- is initialized with a constant expression,
- is not odr-used in any definition of D, and
- has the same value in all definitions of D,
defaultValue
satisfies all but the last point, which it most definitely does not.