7

in visual studio you can set different compiler options for individual cpp files. for example: under "code generation" we can enable basic runtime checks in debug mode. or we can change the floating point model (precise/strict/fast). these are just examples. there are plenty of different flags.

an inline function can be defined multiple times in the program, as long as the definitions are identical. we put this function into a header and include it in several translation units. now, what happens if different compiler options in different cpp files lead to slightly different compiled code for the function? then they do differ and we have undefined behaviour? you could make the function static (or put it into an unnamed namespace) but going further, every memberfunction defined directly in a class is implicit inline. this would mean that we may only include classes in different cpp files if these cpp files share the identical compiler flags. i can not imagine this to be true, because this would basically be to easy to get wrong.

are we really that fast in the land of undefined behaviour? or will compilers handle this cases?

Some programmer dude
  • 400,186
  • 35
  • 402
  • 621
phön
  • 1,215
  • 8
  • 20
  • 1
    So, in one compilation unit `sum(2,2)` gives 4, in another it gives 5. I don't see undefined behaviour here, – Alex F Aug 28 '18 at 06:00
  • @AlexF The problem is [the one definition rule](http://en.cppreference.com/w/cpp/language/definition#One_Definition_Rule). Multiple (differing) definitions in different [translation units](https://en.wikipedia.org/wiki/Translation_unit_(programming)) is UB. Things are a little different with templates, but for non-templated inline functions it might definitively be a problem. – Some programmer dude Aug 28 '18 at 06:03
  • @AlexF if we declare the function static (to have internal linkage), you are right. but i am asking about inline functions – phön Aug 28 '18 at 06:11
  • @Someprogrammerdude: of course, this is problem, but is this UB? In your first link One Definition Rule for inline functions is restricted to every translation unit. – Alex F Aug 28 '18 at 06:16
  • 1
    @AlexF It says that an inline function must be defined in every translation unit where it is odr-used, and the definitions must fulfill the conditions in the bullet list (which essentially says that all definitions must look the same and be equivalent). – molbdnilo Aug 28 '18 at 06:41
  • 2
    I think this is kind of a grey area. The ODR requirements concern the form and semantics of the multiple definitions, not the generated code. – molbdnilo Aug 28 '18 at 06:51
  • 1
    @Someprogrammerdude I don't see how this could be resolved with any amount of standard interpretations, seeing as how compiler flags don't even exist as far as the language is concerned. – Passer By Aug 28 '18 at 13:39
  • @molbdnilo The ODR purpose is that multiple compilations of the same function have no distinct traits. – curiousguy Sep 23 '18 at 13:47
  • If you have non deterministic fp behavior, any inline function returned a float potentially breaks the semantic ODR. – curiousguy Sep 23 '18 at 13:48

3 Answers3

3

As far as the Standard is concerned, each combination of command-line flags turns a compiler into a different implementation. While it is useful for implementations to be able to use object files produced by other implementations, the Standard imposes no requirement that they do so.

Even in the absence of in-lining, consider having the following function in one compilation unit:

char foo(void) { return 255; }

and the following in another:

char foo(void);
int arr[128];
void bar(void)
{
  int x=foo();
  if (x >= 0 && x < 128)
     arr[x]=1;
}

If char was a signed type in both compilation units, the value of x in the second unit would be less than zero (thus skipping the array assignment). If it were an unsigned type in both units, it would be greater than 127 (likewise skipping the assignment). If one compilation unit used a signed char and the other used unsigned, however, and if the implementation expected return values to sign-extended or zero-extended in the result register, the result could be that a compiler might determine that x can't be greater than 127 even though it holds 255, or that it couldn't be less than 0 even though it holds -1. Consequently, the generated code might access arr[255] or arr[-1], with potentially-disastrous results.

While there are many cases where it should be safe to combine code using different compiler flags, the Standard makes no effort to distinguish those where such mixing is safe from those where it is unsafe.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • So rule of thumb is to not alter flags in individual translation units. If this is as strict as it sounds, why does MSVC allow it? I understand it for warning level and other not so important stuff, but code compatibility breaking flags are ringing my alarm bells. – phön Aug 30 '18 at 07:27
  • 2
    @phön: Some aspects of compilation need to be consistent between translation units; other parts don't. An implementation's documentation should usually make clear which parts are which, but that's a Quality of Implementation issue outside the jurisdiction of the Standard. My point isn't that changing flags is usually dangerous, but it isn't *always* safe. – supercat Aug 30 '18 at 14:45
  • @phön: Also, with regard to MSVC, I think that calling conventions used by that compiler in both x86 and x64 mode specify that when a function returns `char` or `unsigned char`, the bottom 8 bits of the extended AX register (EAX or RAX) hold the value and the remaining bits (24 or 56 of them) hold unspecified values. Thus, when using MSVC, a function which returns signed char but is called by code expecting unsigned char, or vice versa, will behave as though the result was converted to the type the caller expects. Other platforms like the ARM, however, would not behave so nicely. – supercat Aug 30 '18 at 16:29
  • "_As far as the Standard is concerned, each combination of command-line flags turns a compiler into a different implementation_" I have no idea where you are reading that. Any combination of flags that change implementation defined aspects (sign of char), yes. – curiousguy Nov 30 '18 at 01:45
  • 2
    @curiousguy: As far as the Standards are concerned, a C or C++ implementation takes a bunch of source files as input, along with any input for the program to be run, and automatically does everything necessary to produces as output whatever the program in question should produce. The Standards have no concept of "persistent" object files, and do not define any means by which any source files or a compiler's configuration could be changed between first the time compilation phase 1 is commenced for any part of a program and the time the program finishes execution. – supercat Nov 30 '18 at 15:56
2

I recently wrote some code for GCC test if this problem actually exists.

SPOILER: it does.

Setup:

I'm compiling some of our code with use of AVX512 instructions. Since most cpus don't support AVX512, we need to compile most of our code without AVX512. The questions is: whether inline function, used in a cpp file compiled with AVX512 can "poison" the whole library with illegal instructions.

Imagine a case where a function from non-AVX512 cpp file calls our function, but it hits an assembly coming from AVX512 compiled unit. This would give us illegal instruction on non AVX512 machines.

Let's give it a try:

func.h

inline void __attribute__ ((noinline)) double_it(float* f) {
  for (int i = 0; i < 16; i++)
    f[i] = f[i] + f[i];
}

We define an inline (in a linker sense) function. Using hard-coded 16 will make GCC optimizer use AVX512 instructions. We have to make it ((noinline)) to prevent the compiler from inlining it (i.e. pasting it's code to callers). This is a cheap way to pretend this function is too long to be worth inlining.

avx512.cpp

#include "func.h"
#include <iostream>

void run_avx512() {
  volatile float f = 1;
  float arr [16] = {f};
  double_it(arr);
  for (int i = 0; i < 16; i++)
    std::cout << arr[i] << " ";
  std::cout << std::endl;
}

This is AVX512 use of our double_it function. It doubles some array and prints the result. We will compile it with AVX512.

non512.cpp

#include "func.h"
#include <iostream>

void run_non_avx() {
  volatile float f = 1;
  float arr [16] = {f};
  double_it(arr);
  for (int i = 0; i < 16; i++)
    std::cout << arr[i] << " ";
  std::cout << std::endl;
}

Same logic as before. This one won't be compiled with AVX512.

lib_user.cpp

void run_non_avx();

int main() {
  run_non_avx();
}

Some user code. Calls `run_non_avx that was compiled without AVX512. It doesn't know it's gonna blob up :)

Now we can compile these files and link them as shared library (probably regular lib would work as well)

g++ -c avx512.cpp -o avx512.o -O3 -mavx512f -g3 -fPIC
g++ -c non512.cpp -o non512.o -O3 -g3 -fPIC
g++ -shared avx512.o non512.o -o libbad.so
g++ lib_user.cpp -L . -lbad -o lib_user.x
./lib_user.x

Running this on my machine (no AVX512) gives me

$ ./lib_user.x
Illegal instruction (core dumped)

On a side note, if I change the order of avx512.o non512.o, it starts working. It seems linker ignores subsequent implementations of the same functions.

S. Kaczor
  • 401
  • 3
  • 8
  • Wierd thing is the problem is already resolved in GCC for LTO optimizations, but not for non-inlined inline functions in headers. Anyway the trick is to force inlining. – Allan Jensen Jul 16 '20 at 13:48
-1

an inline function can be defined multiple times in the program, as long as the definitions are identical

No. ("Identical" isn't even a well defined concept here.)

Formally the definitions must be equivalent in some very strong sense, which doesn't even make sense as a requirement and which nobody cares about:

// in some header (included in multiple TU):

const int limit_max = 200; // implicitly static

inline bool check_limit(int i) {
  return i<=limit_max; // OK
}

inline int impose_limit(int i) {
  return std::min(i, limit_max); // ODR violation
}

Such code is entirely reasonable yet formally violates the one definition rule:

in each definition of D, corresponding names, looked up according to 6.4 [basic.lookup], shall refer to an entity defined within the definition of D, or shall refer to the same entity, after overload resolution (16.3 [over.match]) and after matching of partial template specialization (17.9.3 [temp.over]), except that a name can refer to a 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 (8.20 [expr.const]), and the value (but not the address) of the object is used, and the object has the same value in all definitions of D;

Because the exception doesn't allow using a const object with internal linkage (the const int is implicitly static) for the purpose of directly binding a const reference (and then using the reference for its value only). The correct version is:

inline int impose_limit(int i) {
  return std::min(i, +limit_max); // OK
}

Here the value of limit_max is used in unary operator + and then a const reference is bound to a temporary initialized with that value. Who really does that?

But even the committee doesn't believe the formal ODR matters, as we can see in Core Issue 1511:

1511. const volatile variables and the one-definition rule

Section: 6.2 [basic.def.odr] Status: CD3 Submitter: Richard Smith Date: 2012-06-18

[Moved to DR at the April, 2013 meeting.]

This wording is possibly not sufficiently clear for an example like:

  const volatile int n = 0;
  inline int get() { return n; }

We see that the committee believes that this blatant violation of the intent and purpose of the ODR as written, a code that reads a different volatile object in each TU, that is a code that has a visible side effect on a different object, so a different visible side effect, is OK because we do not care which is which.

What matters is that the effect of the inline function is vaguely equivalent: doing a volatile int read, which is a very weak equivalence, but sufficient for the natural use of the ODR which is instance indifference: which specific instance of the inline function is used doesn't matter and can't make a difference.

In particular the value read by a volatile read is by definition not known by the compiler, so the post condition and invariants of this function as analysed by the compiler are the same.

When using different function definition in different TU, you need to make sure that these are strictly equivalent from the point of view of the caller: that it is never possible to surprise a caller by substituting one for the other. It means that the observable behavior must be strictly the same even if the code is different.

If you use different compiler options, they must not change the range of possible results of a function (possible as viewed by the compiler).

Because the "standard" (which isn't really a specification of a programming language) allows floating point objects to have a real representation not allowed by their officially declared type, in a completely unconstrained way, using any non volatile qualified floating point type in anything multiply defined subject to the ODR seems problematic, unless you activate the "double means double" mode (which is the only sane mode).

curiousguy
  • 8,038
  • 2
  • 40
  • 58