(Note! This question particularly covers the state of C++14, before the introduction of inline variables in C++17)
TLDR; Question
- What constitutes odr-use of a constexpr variable used in the definition of an inline function, such that multiple definitions of the function violates [basic.def.odr]/6?
(... likely [basic.def.odr]/3; but could this silently introduce UB in a program as soon as, say, the address of such a constexpr variable is taken in the context of the inline function's definition?)
TLDR example: does a program where doMath() defined as follows:
// some_math.h #pragma once // Forced by some guideline abhorring literals. constexpr int kTwo{2}; inline int doMath(int arg) { return std::max(arg, kTwo); } // std::max(const int&, const int&) have undefined behaviour as soon as doMath() is defined in two different translation units (say by inclusion of some_math.h and subsequent use of doMath())?
Background
Consider the following example:
// constants.h #pragma once constexpr int kFoo{42}; // foo.h #pragma once #include "constants.h" inline int foo(int arg) { return arg * kFoo; } // #1: kFoo not odr-used // a.cpp #include "foo.h" int a() { return foo(1); } // foo odr-used // b.cpp #include "foo.h" int b() { return foo(2); } // foo odr-used compiled for C++14, particularly before inline variables and thus before constexpr variables were implicitly inline.
The inline function foo (which has external linkage) is odr-used in both translation units (TU) associated with a.cpp and b.cpp, say TU_a and TU_b, and shall thus be defined in both of these TU's ([basic.def.odr]/4).
[basic.def.odr]/6 covers the requirements for when such multiple definitions (different TU's) may appear, and particularly /6.1 and /6.2 is relevant in this context [emphasis mine]:
There can be more than one definition of a [...] inline function with external linkage [...] in a program provided that each definition appears in a different translation unit, and provided the definitions satisfy the following requirements. Given such an entity named D defined in more than one translation unit, then
/6.1 each definition of D shall consist of the same sequence of tokens; and
/6.2 in each definition of D, corresponding names, looked up according to [basic.lookup], shall refer to an entity defined within the definition of D, or shall refer to the same entity, after overload resolution ([over.match]) and after matching of partial template specialization ([temp.over]), except that a name can refer to a non-volatile const object with internal or no linkage if the object has the same literal type in all definitions of D, and the object is initialized with a constant expression ([expr.const]), and the object is not odr-used, and the object has the same value in all definitions of D; and
...
If the definitions of D do not satisfy these requirements, then the behavior is undefined.
/6.1 is fulfilled.
/6.2 if fulfilled if kFoo in foo:
- [OK] is const with internal linkage
- [OK] is initialized with a constant expressions
- [OK] is of same literal type over all definitions of
foo - [OK] has the same value in all definitions of
foo - [??] is not odr-used.
I interpret 5 as particularly "not odr-used in the definition of foo"; this could arguably have been clearer in the wording. However if kFoo is odr-used (at least in the definition of foo) I interpret it as opening up for odr-violations and subsequent undefined behavior, due to violation of [basic.def.odr]/6.
Afaict [basic.def.odr]/3 governs whether kFoo is odr-used or not,
A variable x whose name appears as a potentially-evaluated expression ex is odr-used by ex unless applying the lvalue-to-rvalue conversion ([conv.lval]) to x yields a constant expression ([expr.const]) that does not invoke any non-trivial functions and, if x is an object, ex is an element of the set of potential results of an expression e, where either the lvalue-to-rvalue conversion ([conv.lval]) is applied to e, or e is a discarded-value expression (Clause [expr]). [...]
but I'm having a hard time to understand whether kFoo is considered as odr-used e.g. if its address is taken within the definition of foo, or e.g. whether if its address is taken outside of the definition of foo or not affects whether [basic.def.odr]/6.2 is fulfilled or not.
Further details
Particularly, consider if foo is defined as:
// #2 inline int foo(int arg) { std::cout << "&kFoo in foo() = " << &kFoo << "\n"; return arg * kFoo; } and a() and b() are defined as:
int a() { std::cout << "TU_a, &kFoo = " << &kFoo << "\n"; return foo(1); } int b() { std::cout << "TU_b, &kFoo = " << &kFoo << "\n"; return foo(2); } then running a program which calls a() and b() in sequence produces:
TU_a, &kFoo = 0x401db8 &kFoo in foo() = 0x401db8 // <-- foo() in TU_a: // &kFoo from TU_a TU_b, &kFoo = 0x401dbc &kFoo in foo() = 0x401db8 // <-- foo() in TU_b: // !!! &kFoo from TU_a namely the address of the TU-local kFoo when accessed from the different a() and b() functions, but pointing to the same kFoo address when accessed from foo().
DEMO.
Does this program (with foo and a/b defined as per this section) have undefined behaviour?
A real life example would be where these constexpr variables represent mathematical constants, and where they are used, from within the definition of an inline function, as arguments to utility math functions such as std::max(), which takes its arguments by reference.
arg * kFoodoesn't use adress ofkFoo, so it should be fine. returningstd::max(arg, kFoo);would odr-usekFoo(std::maxtakes its argument by const reference)std::maxexample: this example (which I see not too uncommonly in C++14 code bases, prior to C++17'sconstexprstd::maxand so on) is then UB as soon as the function is defined in more than one translation unit? Particularly, the AUTOSAR guidelines for use of C++14 in safety critical systems enforce the following (required) rule, "A5-1-1: Literal values shall not be used apart from type initialization, otherwise symbolic names shall be used instead.", which typically results in various constants such asconstexpr int kTwo{2};when adhered to.