4

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 bin2c to generate various header files that define various different long strings.
  • I have a statically linked library 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.
  • 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.cpp into lib.a.
  • Generate Doc1.h from doc1.txt using bin2c-ish.
  • Generate Doc2.h from doc2.txt using bin2c-ish.
  • Generate Doc3.h from doc3.txt using bin2c-ish.
  • Compile my_app1.cpp into my_app1_default.a and link with lib.a into my_app1_default.
  • Compile my_app1.cpp with -DGENERATED_HEADER=\"Doc1.h\" into my_app2_doc1.a and link with lib.a into my_app1_doc1.
  • Compile my_app1.cpp with -DGENERATED_HEADER=\"Doc2.h\" into my_app1_doc2.a and link with lib.a into my_app1_doc2.
  • Compile my_app2.cpp into my_app2_default.a and link with lib.a into my_app2_default.
  • Compile 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 :)

6
  • 1
    Note that in general default arguments are resolved at the call site, according to the declaration as visible there. Commented Apr 5 at 4:35
  • @wohlstad Thank you. So I think that implies the answer is 'yes and yes'? Or is there another complication? Commented Apr 5 at 4:37
  • "The goal here is to make the default behavior of a function customizable" just use a macro that resolves to the correct function. You are courting ODR-violations here. Commented Apr 5 at 4:44
  • @PasserBy This is a simplified example. In my use case the resulting behavior is more complex than resolving to one of two functions. Essentially the value of defaultValue is injected by the build environment. But why would this court ODR-violations? The function is defined exactly once, and defaultValue is internal to each TU. Commented Apr 5 at 4:49
  • 1
    The only potential complication is if the default value is specified in the function definition, and differs from the default value visible when the function is called. For example, void func(int x = 42) {std::cout << x;} and another source file sees a declaration void func(int x = 314); int main() {func();}, then the value printed will be 314, not 42. While that is correct according to the standard, it can be problematic if - in an example like this - a programmer incorrectly assumes that 42 will be printed and gets a surprise. Commented Apr 5 at 5:21

1 Answer 1

5

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)

basic.def.odr/14

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.

Sign up to request clarification or add additional context in comments.

5 Comments

How is the sort-of-common usage of std::source_location::current() as a default argument differs from this though? That ought not to be an ODR violation as it was kinda specifically designed to be used that way; and if it is not then couldn't OP use the same by wrapping different values into a static consteval accessor function? If current() is implementable somehow then one outght to be able to do something similar? Unless it is special-cased out through some builtin magic...
@AndreyTurkin The same function std::source_location::current() is being referred to at each call site, so "shall refer to the same entity" is already satisfied. It's just returning a different value at each invocation. There is magic involved, but the magic has to do with how the function is implemented, not whether it's allowed in the first place.
Excellent answer, thank you.
@PasserBy indeed it is, I missed that source_location is a structure not a namespace. I've looked at gcc and msvc and in both cases it is defined essentially as static consteval current(int line = __builtin_LINE(), _bunch more_) { return source_location {.line = line, ...};}. So it kind of pushes the question one step further. IT is the same single function but how ITS default arguments are OK? I guess this is there we get "its builtin magic" stuff where __builtin_LINE and co. are simultaneously single pure consteval functions that are OK to be used, and yet they compute to different values.
And this is how they stopped this potentially different values leaking from these magic functions upstream... cplusplus.github.io/CWG/issues/2678.html . Now I understand why there is such a rule. It's so what someone couldn't circumvent ODR when calling such functions from odr-defined function. They could've made the rules more lax (e.g. by requiring default arguments to be odr safe if they are used by odr defined function, just like they did with consteval) but that probably would've been seen as too indirect.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.