0

The setup (source files are listed at the bottom). There is a Counters class with 2 counter members and 2 methods to increment them and a 3rd extra #ifndef NDEBUG-guarded "debug" counter and its incrementer. These incrementer methods are provided through additional functions defined in their respective headers.

The bug. This is a contrived scenario, but the "-DNDEBUG" compilation flag is provided for only one of the two headers. The effect of this is that the wrong counter is incremented in the final program: main.cpp prints:

$ CFLAGS='-DNDEBUG' make; ./main

> Counter 1: 0
> Counter 2: 4
> Debug all counters: 7

main.cpp:

#include<iostream>
#include "counters.hpp"
#include "increment1.hpp"
#include "increment2.hpp"
using namespace std;

int main(int argc, char *argv[]){

Counters c;

increment1(c);increment1(c);increment1(c);
increment2(c);increment2(c);increment2(c);increment2(c);

cout << "Counter 1: " << c.getCounter1() << endl;
cout << "Counter 2: " << c.getCounter2() << endl;

#ifndef NDEBUG
cout << "Debug all counters: " << c.getDebugAllCounters() << endl;
#endif

return 0;}

Purportedly, this is because when increment1.cpp receives the "no-debug" version of the Counters class and is therefore unaware of the extra debug counter, which is now the 1st member of the struct, and erroneously increments it instead of m_counter. (This is easily checked by moving the the debug counter below the two main counters)

I'm imagining that this is because clang++ -c -DNDEBUG -O2 increment1.cpp builds increment1.cpp (see Makefile below) with a model of Counters which only has two (instead of three) classes, but in the final main.cpp as well as increment2.cpp there are three memebers. So

// increment1.cpp
#include "counters.hpp"
void increment1(Counters& counters){counters.inc1();}

actually turns into pseudocode

// .. 
// .. whole non-debug version of Counters
// ..
void increment1(Counters& counters){
       [the Counters instance]->[first_member]++;}
}

Q1

How does this inlining of the class method into a function call take place? Does the differing names of members not matter in this case because inlining takes place at the register level? Is there a compiler flag (like -S) I can use to see this intermediate inlined representation?

Q2

This error doesn't go away even if i replace all -O2's with -O0's in the Makefile or add __attribute__((optnone)) to inc1. If inlining is the culprit, what am i missing?


Counters.hpp
increment1.cpp
increment1.hpp
increment2.cpp
increment2.hpp
main.cpp

Counters.hpp

class Counters
{
private:
    #ifndef NDEBUG   
        int m_debugAllCounters;  
    #endif
    int m_counter1;
    int m_counter2;
public:
    Counters() :
    #ifndef NDEBUG 
        m_debugAllCounters(0),
    #endif
                 m_counter1(0), m_counter2(0){}
     void inc1(){
        #ifndef NDEBUG  
            m_debugAllCounters++;
        #endif
        m_counter1++;}

     void inc2(){
        #ifndef NDEBUG  
            m_debugAllCounters++;
        #endif
        m_counter2++; }

    int getCounter1() const { return m_counter1; }
    int getCounter2() const { return m_counter2; }
    #ifndef NDEBUG
    int getDebugAllCounters() const { return m_debugAllCounters; }
    #endif
};

increment1.hpp

class Counters;
int increment1(Counters&);

increment1.cpp

#include "counters.hpp"
void increment1(Counters& counters){counters.inc1();}

increment2.hpp

class Counters;
int increment2(Counters&);

increment2.cpp

#include "counters.hpp"
void increment2(Counters& counters){counters.inc2();}

Makefile

all: main.o increment1.o increment2.o
    clang++ -o main main.o increment1.o increment2.o

main.o: main.cpp increment1.hpp increment2.hpp counters.hpp
    clang++ -c -O2 main.cpp

increment1.o: increment1.cpp counters.hpp
    clang++ -c $(CFLAGS) -O2 increment1.cpp

increment2.o: increment2.cpp counters.hpp
    clang++ -c -O2 increment2.cpp

clean:
    rm -f *.o diff-flags

This bug is outlined in the following article("Compiling with different flags" section at the bottom).

rtviii
  • 807
  • 8
  • 19
  • 2
    `-DNDEBUG` changes the class ABI, all files need to be rebuilt. Just `CFLAGS='-DNDEBUG' make` doesn't rebuilt all files. – 273K Jul 18 '23 at 14:15
  • Any idea how i could inspect the ABI with and without the flag, @273K? Thanks! – rtviii Jul 18 '23 at 14:16
  • 3
    You could use the [ORC](https://github.com/adobe/orc) tool to detect ODR violations. Titus Winters would say, "Build EVERYTHING from source (dammit), with the same flags (dammit), at the same time (dammit)!" – Eljay Jul 18 '23 at 14:19
  • 1
    Don't change the class ABI at the beginning if you are unable to rebuilt all files. Always append new members at the end. – 273K Jul 18 '23 at 14:20
  • 1
    "inspect" the ABI? Look at the asm ([How to remove "noise" from GCC/clang assembly output?](https://stackoverflow.com/q/38552116)), and/or print `offsetof(Counters, m_counter1)` – Peter Cordes Jul 18 '23 at 16:34

0 Answers0