Is it safe to use #ifdef guards on C++ class member functions?
Asked Answered
T

3

18

Suppose you have the following definition of a C++ class:

class A {
// Methods
#ifdef X
// Hidden methods in some translation units
#endif
};

Is this a violation of One Definition Rule for the class? What are the associated hazards? I suspect if member function pointers or virtual functions are used this will most likely break. Otherwise is it safe to use?

I am considering it in the context of Objective C++. The header file is included in both pure C++ and Objective C++ translation units. My idea is to guard methods with Objective-C types with OBJC macro. Otherwise, I have to use void pointer for all Objective-C types in the header, but this way I am losing strong typing and also ugly static casts must be added all over the code.

Terrific answered 4/2, 2021 at 6:47 Comment(9)
It's definitely a violation of ODR. It might be safe on a given compiler but why bother? What are you gaining with this? Inheritance seems like the correct way to achieve the same effect.Lucianolucias
It's only "safe" as long as all translation units that use this header have X defined (or not). Since ensuring that is quite a task, I wouldn't call it safe at all.Adiaphorism
What could be the use of this? Can private: not do it? Whom you are hiding them from, as anybody that has the . File can read it?Warty
No, it's not safe, unless X is defined in exactly the same way in all compilation units. It breaches the one definition rule, so the behaviour of your program is undefined. Under the one definition rule, class definitions seen by all compilation units must be identical (in terms of the set of tokens that, collectively, form the class definition). Having one version of a class with declarations of its member functions and one version without breaks that rule.Neoteny
and it would be absolutely your responsibility that object's layout matches between units written in different languages, not to mention having different calling convention used. You need some wrappers to use C++ from ObjectiveC and vice versa, such questions were asked before, e.g. #1061505Sedgewick
It may blow up horribly if the first virtual function is inside the #ifdef/#endif block. You may find duplicate definitions of the virtual function table with different indexes.Mediator
Without motivations and use cases in the question itself, it is unclear and lacks details. I voted to close it, after having asked for improvementEonian
Do you really need member functions? The std library and many libraries that use the same coding style like the std library only add a bare minimum of member functions to the types. Everything else is done with free functions.Confer
Maybe the answers to this question gives you hints.Aircraftman
S
17

Yes, it MAY allow a hazard of ODR breach if separate compilation units are allowed to have different state of macro definition X. X should be defined (or not defined) globally across program (and shared objects) before every inclusion of that class definition for program to meet requirement to be compliant. As far as C++ compiler (not preprocessor) concerned, those are two different, incompatible, unrelated class types.

Imagine situation where in compilation unit A.cpp X was defined before class A and in unit B.cpp X was not defined. You would not get any compiler errors if nothing within B.cpp uses those members which were "removed". Both units may be considered well-formed on their own. Now if B.cpp would contain a new expression, it would create an object of incompatible type, smaller than one defined in A.cpp. But any method from class A , including constructor, may cause an UB by accessing memory outside of object's storage when called with object created in B.cpp, because they use the larger definition.

There is a variation of this folly, an inclusion of header file's copy into two or more different folders of build tree with same name of file and POD struct type, one of those folders accessible by #include <filename>. Units with #include "filename" designed to use alternatives. But they wouldn't. Because order of header file lookup in this case is platform-defined, programmer isn't entirely in control of which header would be included in which unit on every platform with #include "filename". As soon as one definition would be changed, even just by re-ordering members, ODR was breached.

To be particularly safe, such things should be done only in compiler domain by using templates, PIMPL, etc. For inter-language communication some middle ground should be arranged, using wrappers or adapters, C++ and ObjectiveC++ may have incompatible memory layout of non-POD objects.

Sedgewick answered 4/2, 2021 at 6:56 Comment(8)
In all implementations known to mankind the number of member functions does not change the object size.Aircraftman
@Peter-ReinstateMonica Except for the first virtual member function which adds vtable pointer to the object, right?Longmire
@Longmire Correct. I meant non-virtual functions (and for virtual ones your statement holds).Aircraftman
I specifically added a possibility of memory layout change: virtual methods, inheritance, const-ness and inclusion of non-POD members affects thatSedgewick
@Longmire Any virtual function except the last most likely. If the compiler sees virtual functions a, b, c it will put a, b, c into the vtable in that order. If it sees a and c only in another compilation unit, it will assume only a and c in the vtable. So a call to c() will actually call b().Toughminded
@Toughminded But whenever it is a+b+c or just a+c, the object size will increase the same, by single pointer to vtable, as long as there is at least one virtual function. The size and integrity of vtable is yet another possible issue. Offtopic, but why compiler would pick 3 function (a+b+c) over 2 function (a+c) vtable during linking? I suppose that vtable has to fullfill ODR, so we discuss about UB, so it cannot be determined, whenever b() call would be c() call or c() would try to execute non-existing function.Longmire
@Longmire vtable are essentially arrays or maps of pointers. there is own version of vtable is considered, one unit uses one, which refers symbols _impl_A_a and _impl_A_b (mangled), the other refers _impl_A_a,_impl_A_b and _impl_A_c. No compiling on linking problems, but during execution if fails when attempts to address outside of the former table's instance to get function address and reads wrong data, assuming it is latter. It really depends if constructor was inlined or not as well, inlined constructor would create vtable visible from outsider unit.Sedgewick
Thanks for explanation, it's even more complicated than I always thought - gonna dig and read more into it :)Longmire
M
15

This blows up horribly. Do not do this. Example with gcc:

Header file:

// a.h

class Foo
{
public:
    Foo() { ; }

#ifdef A
    virtual void IsCalled();
#endif
    virtual void NotCalled();
};

First C++ File:

// a1.cpp

#include <iostream>

#include "a.h"

void Foo::NotCalled()
{
    std::cout << "This function is never called" << std::endl;
}

extern Foo* getFoo();
extern void IsCalled(Foo *f);

int main()
{
   Foo* f = getFoo();
   IsCalled(f);
}

Second C++ file:

// a2.cpp

#define A
#include "a.h"
#include <iostream>

void Foo::IsCalled(void)
{
    std::cout << "We call this function, but ...?!" << std::endl;
}

void IsCalled(Foo *f)
{
    f->IsCalled();
}

Foo* getFoo()
{
    return new Foo();
}

Result:

This function is never called

Oops! The code called virtual function IsCalled and we dispatched to NotCalled because the two translation units disagreed on which entry was where in the class virtual function table.

What went wrong here? We violated ODR. So now two translations units disagree on what is supposed to be where in the virtual function table. So if we create a class in one translation unit and call a virtual function in it from another translation unit, we may call the wrong virtual function. Oopsie whoopsie!

Please do not deliberately do thigs that the relevant standards say are not allowed and will not work. You will never be able to think of every possible way it can go wrong. This kind of reasoning has caused many disasters over my decades of programming and I really wish people would stop deliberately and intentionally creating potential disasters.

Mediator answered 4/2, 2021 at 7:44 Comment(8)
Yeah, don't do that with virtual member functions. Any qualms in practice about regular member functions?Aircraftman
@Peter-ReinstateMonica There's no fundamental difference between virtual and regular member functions and decades of practice has taught me that relying on the fact that I can't think of a way something can break is a on-starter. I'm not clever enough to do that.Mediator
"There's no fundamental difference between virtual and regular member functions": That's so not true.Aircraftman
@Peter-ReinstateMonica Please explain the relevant fundamental difference. Fundamentally, neither is guaranteed to work in this case.Mediator
You know as well as I do that the relevant fundamental difference is in an additional indirection for virtual functions, necessitating a run time mechanism with type information to resolve calls. Purely conceptually they can not generally be resolved at compile time. Non-virtual functions, by contrast, can be resolved at compile time. This is important for performance; it is the reason for Bjarne's conscious design decision to have them at all and not simply make all member functions virtual. Consequently all implementations exploit that and do resolve calls at compile time.Aircraftman
(ctd.) For this, they use the ancient C mechanisms: Member functions (not telling you anything new here) are from the link perspective freestanding functions which are resolved by symbol name. This fundamental difference is relevant here because for the standard C function call resolution mechanism the order of declaration is irrelevant. It is, typically, relevant for the run time mechanism of virtual calls though. Since you know all this I'm genuinely unsure what your motivation was to make that assertion or ask that question ;-).Aircraftman
@Peter-ReinstateMonica I'm sorry, I don't agree. Virtual functions can be resolved at compile/link time. In my example, only one class instance is ever created, so the compiler definitely could know the type of the object the virtual function is called on. All of your arguments are based on things that you "know" that are just based on how things you happen to be familiar with happen to work today. Conceptually, there's nothing beyond the compiler/optimizer's knowledge. My motivation is to stop new instances of the multiple disasters I've seen over my decades of experience where ...Mediator
@Peter-ReinstateMonica ... people relied on things explicitly not guaranteed because they happened to be true in their limited experience. And it really bothers me when people explicitly advocate causing more of these disasters by relying on things that just happen to be true on the systems you just happen to use.Mediator
E
1

Is it safe to use #ifdef guards on C++ class member functions?

In practice (look at the generated assembler code using GCC as g++ -O2 -fverbose-asm -S) what you propose to do is safe. In theory it should not be.

However, there is another practical approach (used in Qt and FLTK). Use some naming conventions in your "hidden" methods (e.g. document that all of them should have dontuse in their name like int dontuseme(void)), and write your GCC plugin to warn against them at compile time. Or just use some clever grep(1) in your build process (e.g. in your Makefile)

Alternatively, your GCC plugin may implement new #pragma-s or function attributes, and could warn against misuse of such functions.

Of course, you can also use (cleverly) private: and most importantly, generate C++ code (with a generator like SWIG) in your build procedure.

So practically speaking, your #ifdef guards may be useless. And I am not sure they make the C++ code more readable.

If performance matters (with GCC), use the -flto -O2 flags at both compile and link time.

See also GNU autoconf -which uses similar preprocessor based approaches.

Or use some other preprocessor or C++ code generator (GNU m4, GPP, your own one made with ANTLR or GNU bison) to generate some C++ code. Like Qt does with its moc.

So my opinion is that what you want to do is useless. Your unstated goals can be achieved in many other ways. For example, generating "random" looking C++ identifiers (or C identifiers, or ObjectiveC++ names, etc....) like _5yQcFbU0s (this is done in RefPerSys) - the accidental collision of names is then very improbable.

In a comment you state:

Otherwise, I have to use void* for all Objective-C types in the header, but this way I am losing strong typing

No, you can generate some inline C++ functions (that would use reinterpret_cast) to gain again that strong typing. Qt does so! FLTK or FOX or GTKmm also generate C++ code (since GUI code is easy to generate).

My idea was to guard methods with Objective-C types with OBJC macro

This make perfect sense if you generate some C++ or C or Objective C code with these macros.

I suspect if member function pointers or virtual functions are used this will most likely break.

In practice, it won't break if you generate random looking C++ identifiers. Or just if you document naming conventions (like GNU bison or ANTLR does) in generated C++ code (or in generated Objective C++, or in generated C, ... code)

Please notice that compilers like GCC use today (in 2021, internally) several C++ code generators. So generating C++ code is a common practice. In practice, the risks of name collisions are small if you take care of generating "random" identifiers (you could store them in some sqlite database at build time).

also ugly static casts must be added all over the code

These casts don't matter if the ugly code is generated.

As examples, RPCGEN and SWIG -or Bisoncpp- generate ugly C and C++ code which works very well (and perhaps also some proprietary ASN.1 or JSON or HTTP or SMTP or XML related in-house code generators).

The header file is included in both pure C++ and Objective C++ translation units.

An alternative approach is to generate two different header files...

one for C++, and another for Objective C++. The SWIG tool could be inspirational. Of course your (C or C++ or Objective C) code generators would emit random looking identifiers.... Like I do in both Bismon (generating random looking C names like moduleinit_9oXtCgAbkqv_4y1xhhF5Nhz_BM) and RefPerSys (generating random looking C++ names like rpsapply_61pgHb5KRq600RLnKD ...); in both systems accidental name collision is very improbable.

Of course, in principle, using #ifdef guards is not safe, as explained in this answer.

PS. A few years ago I did work on GCC MELT which generated millions of lines of C++ code for some old versions of the GCC compiler. Today -in 2021- you practically could use asmjit or libgccjit to generate machine code more directly. Partial evaluation is then a good conceptual framework.

Eonian answered 4/2, 2021 at 6:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.