Mustafa Kemal GILOR
Mustafa Kemal GILOR

Reputation: 156

Is it possible to merge forward declaration and regular declaration into a single file, then use it as if they're separated?

I have this crazy idea of merging forward declaration headers and actual declaration files into one by performing some macro trickery. To provide some context, the daily policy I follow for forward declarations is as follows;

But having a separate _fwd.hpp header for each header pollutes the project, and is kind of hard to maintain. So, I come up with the following idea of merging forward declaration and actual declaration into single file, then enabling them according to include count. I come up with this initial idea of double-inclusion of header;

foo.hpp

#if !defined(FOO_FWD_H)
#define FOO_FWD_H
    // Forward declarations goes here
    struct foo;
#else // has forward_declaration, include actual if not included yet

#if !defined(FOO_H)
    #define FOO_H
    struct foo{
       foo(){/*....*/}
    };
    // Normal declarations goes here
#endif // FOO_H

#endif // FOO_FWD_H

If I include "foo.hpp" once, I get forward declaration of foo, but if I include it second time in a translation unit, I get the forward & actual declaration of the foo, which is totally fine by me. (as I'm kinda doing the same thing anyway, include fwdecl in header, actual in cpp).

So, when put into use case described above, it goes like this;

bar.hpp

#pragma once

#include "foo.hpp" // forward declaration

struct bar{
    bar(const foo& f);
};

bar.cpp

#include "bar.hpp" // bar + 1st inclusion of foo.hpp
#include "foo.hpp" // included 2nd time, actual implementation enabled

bar::bar(const foo& f){
    f.rab(); // requires actual implementation
}

But as you can imagine, this approach has problems. The biggest issue is, if foo.hpp included by another header, tar.hpp, and tar.hpp is included in bar.hpp, causes actual implementation to be exposed to bar.hpp, defeating the purpose. Also, when actual implementation of foo.hpp is required in bar.hpp, it has to be included twice, which looks weird (linters and tools like iwyu might have an issue with that).

So the question boils down to this, can we actually make this work in such a way that;

Thanks in advance.

UPDATE: (30/10/19 10:57 PM GMT+2)

Improved version of idiom based on @IanAbbott 's answer:

Try it live : repl.it

foo.hpp (our single fwdecl & decl idiom implementing header)

    // (mgilor): we got ourselves quite a lot boilerplate code, 
    // maybe x-macro concept help us to move away boilerplate to
    // a separate file?

    #if defined(FOO_FWD_ONLY)
        #undef FOO_FWD_HPP // prevent accidental implementation inclusion on other headers
    #endif

    #if defined(FOO_FWD_ONLY) && !defined(FOO_FWD_HPP) 
        #define FOO_FWD_HPP 
        // forward declarations go here
        struct foo;
    #elif !defined(FOO_FWD_ONLY)

         // includer wants the full monty
        #if !defined(FOO_HPP)
            #define FOO_HPP
            // actual declarations go here

            struct foo{
                foo(){/*....*/}
                void do_things(){}
            };

        #endif // FOO_HPP

    #endif // FOO_FWD_HPP

    // undef the macro, so future includes does not get affected
    #undef FOO_FWD_ONLY

tar.hpp (fwdecl only consumer of foo)

    #pragma once

    #define FOO_FWD_ONLY
    #include "foo.hpp" // this header needs forward declaration

    #ifdef FOO_FWD_HPP 
        #pragma message ( __FILE__ " has forward declaration of foo") 
    #endif
    #ifdef FOO_HPP
        #pragma message ( __FILE__ " has full declaration of foo") 
    #endif

    struct tar{
        tar(foo & f){ }
    };

bar.hpp (fwdecl only consumer of foo, also consumes tar.hpp)

    #pragma once

    #include "tar.hpp" // tar consumed foo fwdecl-only
    #define FOO_FWD_ONLY
    #include "foo.hpp" // bar needs fwdecl-only

    #ifdef FOO_FWD_HPP 
        #pragma message ( __FILE__ " has forward declaration of foo") 
    #endif
    #ifdef FOO_HPP
        #pragma message ( __FILE__ " has full declaration of foo") 
    #endif

    struct bar{
        bar(foo & f);
    };

bar.cpp (full decl consumer of bar & foo)

    #include "bar.hpp"
    #include "foo.hpp" // second inclusion, should enable full definition

    #ifdef FOO_FWD_HPP 
        #pragma message ( __FILE__ " has forward declaration of foo") 
    #endif
    #ifdef FOO_HPP
        #pragma message ( __FILE__ " has full declaration of foo") 
    #endif

    bar::bar(foo& ref){
        ref.do_things();
    }

baz.hpp (no dependencies)

    #pragma once

    struct baz{
        void do_baz();
    };

baz.cpp (full decl consumer of foo & baz)

    #include "baz.hpp"
    #include "foo.hpp"  // no prior include of foo, but since FOO_FWD_ONLY is not defined
                        // baz.cpp will get full declaration.

    #ifdef FOO_FWD_HPP 
        #pragma message ( __FILE__ " has forward declaration of foo") 
    #endif
    #ifdef FOO_HPP
        #pragma message ( __FILE__ " has full declaration of foo") 
    #endif


    void baz::do_baz(){
        foo f;
        f.do_things(); // completely fine.
    }

main.cpp (consuming application)

    // consuming application
    #include "tar.hpp" 
    #include "bar.hpp" 
    #include "foo.hpp"  // already has previous foo fwdecl, so second inclusion will enable full declaration. 
                        // (also FOO_FWD_ONLY is not defined, so first inclusion would enable it too)
    #include "baz.hpp"
    int main(void){

        foo f;
        tar t(f);
        bar b(f);
        baz bz;
    }

Output when compiled:

    tar.hpp:7:13: warning: tar.hpp has forward declaration of foo 
    bar.hpp:8:13: warning: bar.hpp has forward declaration of foo 
    bar.cpp:6:13: warning: bar.cpp has forward declaration of foo 
    bar.cpp:9:13: warning: bar.cpp has full declaration of foo 
    baz.cpp:9:13: warning: baz.cpp has full declaration of foo 
    tar.hpp:7:13: warning: tar.hpp has forward declaration of foo 
    bar.hpp:8:13: warning: bar.hpp has forward declaration of foo 

Upvotes: 1

Views: 319

Answers (1)

Ian Abbott
Ian Abbott

Reputation: 17403

Here is a proposal for consideration (maybe not the best). It involves the includer of an include file setting a macro to indicate that it only needs the forward declarations from that include file. When the macro is not defined, it gets everything from the include file. To avoid problems with header files forgetting to undefine the special macro afterwards, the file being included can be made responsible for undefining it.

It goes something like this:

foo.hpp

#if !defined(FOO_FWD_HPP)
#define FOO_FWD_HPP

// forward declarations go here

struct foo;

#endif // FOO_FWD_HPP

#if !defined(FOO_FWD_ONLY)
// includer wants the full monty
#if !defined(FOO_HPP)
#define FOO_HPP

// normal declarations go here

struct foo{
   foo(){/*....*/}
};

#endif // FOO_HPP
#endif // FOO_FWD_ONLY

#undef FOO_FWD_ONLY

bar.hpp

#pragma once

// only need forward declarations from foo.hpp
#define FOO_FWD_ONLY
#include "foo.hpp"

struct bar {
    bar(const foo& f);
};

bar.cpp

#include "bar.hpp"
#include "foo.hpp"

bar::bar(const foo& f){
    f.rab(); // requires actual implementation
}

The main benefit is to reduce the amount of code being compiled. It does not do anything to fix the problem of unintended exposure. For example, if "bar.hpp" includes some other file that includes "foo.hpp" without defining the FOO_FWD_ONLY macro first, the full definitions from "foo.hpp" will be exposed to the remainder of "bar.hpp".

Upvotes: 1

Related Questions