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.
Context for the question
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:
- I'm using something similar to
bin2cto generate various header files that define various different long strings. - I have a statically linked library
libthat 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. - I'm compiling multiple different version of multiple different binaries, each with different auto-generated header files, each linked to lib.
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:
- Compile
lib.cppintolib.a. - Generate
Doc1.hfromdoc1.txtusing bin2c-ish. - Generate
Doc2.hfromdoc2.txtusing bin2c-ish. - Generate
Doc3.hfromdoc3.txtusing bin2c-ish. - Compile
my_app1.cppintomy_app1_default.aand link withlib.aintomy_app1_default. - Compile
my_app1.cppwith-DGENERATED_HEADER=\"Doc1.h\"intomy_app2_doc1.aand link withlib.aintomy_app1_doc1. - Compile
my_app1.cppwith-DGENERATED_HEADER=\"Doc2.h\"intomy_app1_doc2.aand link withlib.aintomy_app1_doc2. - Compile
my_app2.cppintomy_app2_default.aand link withlib.aintomy_app2_default. - Compile
my_app2.cppwith-DGENERATED_HEADER=\"Doc3.h\"intomy_app2_doc3.aand link withlib.aintomy_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 :)
defaultValueis injected by the build environment. But why would this court ODR-violations? The function is defined exactly once, anddefaultValueis internal to each TU.void func(int x = 42) {std::cout << x;}and another source file sees a declarationvoid func(int x = 314); int main() {func();}, then the value printed will be314, not42. While that is correct according to the standard, it can be problematic if - in an example like this - a programmer incorrectly assumes that42will be printed and gets a surprise.