7

(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:

  1. [OK] is const with internal linkage
  2. [OK] is initialized with a constant expressions
  3. [OK] is of same literal type over all definitions of foo
  4. [OK] has the same value in all definitions of foo
  5. [??] 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.

3
  • arg * kFoo doesn't use adress of kFoo, so it should be fine. returning std::max(arg, kFoo); would odr-use kFoo (std::max takes its argument by const reference) Commented Sep 8, 2021 at 15:18
  • Tangent: constexpr variables are only implicitly inline if they are static member variables. Commented Sep 8, 2021 at 15:19
  • @Jarod42 I just updated with a TLDR std::max example: this example (which I see not too uncommonly in C++14 code bases, prior to C++17's constexpr std::max and 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 as constexpr int kTwo{2}; when adhered to. Commented Sep 8, 2021 at 15:20

2 Answers 2

5

In the OP's example with std::max, an ODR violation does indeed occur, and the program is ill-formed NDR. To avoid this issue, you might consider one of the following fixes:

  • give the doMath function internal linkage, or
  • move the declaration of kTwo inside doMath

A variable that is used by an expression is considered to be odr-used unless there is a certain kind of simple proof that the reference to the variable can be replaced by the compile-time constant value of the variable without changing the result of the expression. If such a simple proof exists, then the standard requires the compiler perform such a replacement; consequently the variable is not odr-used (in particular, it does not require a definition, and the issue described by the OP would be avoided because none of the translation units in which doMath is defined would actually reference a definition of kTwo). If the expression is too complicated, however, then all bets are off. The compiler might still replace the variable with its value, in which case the program may work as you expect; or the program may exhibit bugs or crash. That's the reality with IFNDR programs.

The case where the variable is immediately passed by reference to a function, with the reference binding directly, is one common case where the variable is used in a way that is too complicated and the compiler is not required to determine whether or not it may be replaced by its compile-time constant value. This is because doing so would necessarily require inspecting the definition of the function (such as std::max<int> in this example).

You can "help" the compiler by writing int(kTwo) and using that as the argument to std::max as opposed to kTwo itself; this prevents an odr-use since the lvalue-to-rvalue conversion is now immediately applied prior to calling the function. I don't think this is a great solution (I recommend one of the two solutions that I previously mentioned) but it has its uses (GoogleTest uses this in order to avoid introducing odr-uses in statements like EXPECT_EQ(2, kTwo)).

If you want to know more about how to understand the precise definition of odr-use, involving "potential results of an expression e...", that would be best addressed with a separate question.

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

Comments

0

Does a program where doMath() defined as follows: [...] 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())?

Yes; this particular issue was highlighted in LWG2888 and LWG2889 which were both resolved for C++17 by P0607R0 (Inline Variables for the Standard Library) [emphasis mine]:

2888. Variables of library tag types need to be inline variables

[...]

The variables of library tag types need to be inline variables. Otherwise, using them in inline functions in multiple translation units is an ODR violation.

Proposed change: Make piecewise_construct, allocator_arg, nullopt, (the in_place_tags after they are made regular tags), defer_lock, try_to_lock and adopt_lock inline.

[...]

[2017-03-12, post-Kona] Resolved by p0607r0.

2889. Mark constexpr global variables as inline

The C++ standard library provides many constexpr global variables. These all create the risk of ODR violations for innocent user code. This is especially bad for the new ExecutionPolicy algorithms, since their constants are always passed by reference, so any use of those algorithms from an inline function results in an ODR violation.

This can be avoided by marking the globals as inline.

Proposed change: Add inline specifier to: bind placeholders _1, _2, ..., nullopt, piecewise_construct, allocator_arg, ignore, seq, par, par_unseq in

[...]

[2017-03-12, post-Kona] Resolved by p0607r0.

Thus, in C++14, prior to inline variables, this risk is present both for your own global variables as well as library ones.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.