Reputation: 156
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
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