Jump to content
  • Advertisement
  • 11/17/15 04:16 AM
    Sign in to follow this  

    Making Your C++ Namespace Solid and Future-Proof

    General and Gameplay Programming

    DemonDar

    This article provides a possible solution to a real problem, nothing more, nothing less. It is up to developers evaluating pros and cons to decide if this approach is worthwhile for their framework/library. The final goal is to mimik the "import" feature of languages like Java or C#. That will be usefull for large codebases or standard codebases, but it is of no use for small code-bases or home-made projects.

    The problem

    In C++ you do not import stuff, you include files which means "text replacement" and some preprocessor magic. When you include a file you are indirectly including many other files, relying on this behaviour is bad and can cause harm in the long run:
    • What you expect "by design" is that you have at your disposal only what you "imported/included"
    • Side-included headers are actually only a implementation detail: it may change!

    Two real examples of breaking code

    Example #1 This GCC distribution at least have a STL library that indirectly include from other headers, when you accidentally use stuff from such a header then the code will just compile fine, but when you try to compile the code from elsewhere the compilers will complain that there is no such thing called "std::function" and (maybe) you are missing some include (and you are truly missing an include). Example #2 Your class is using another class as a private member: #include "Foo.h" // a implementation detail class MyClass{ Foo _foo; public: //... }; Later you decide to refactor the code and use internally another class: #include "Bar.h" //ops.. any client used "foo" but not included it? => dang compile error for him class MyClass{ Bar _bar; public: //... };

    The Solution

    The solution to the problem is actually very simple: Put everything in another namespace, and "import" it only if client is actually including it from the right header.

    Your library BEFORE

    Directory structure: mylib/ +MyClass.h +Foo.h +MyClass.cpp +Foo.cpp MyClass.h: including this file actually cause the inclusion of "Foo.h". #pragma once #include "Foo.h" // a implementation detail namespace mylib{ class MyClass{ Foo _foo; public: //... }; } MyClass.cpp #include "MyClass.h" // a implementation detail namespace mylib{ //... } Foo.h #pragma once namespace mylib{ class Foo{ //... }; }

    Your library AFTER

    Directory structure: mylib/ +MyClass.h +Foo.h priv/ +MyClass.h +Foo.h +MyClass.cpp +Foo.cpp You move all old files to a private folder, then you just import stuff into your namespace from public headers Forwarding headers mylib/MyClass.h #include "priv/MyClass.h" namespace PUBLIC_NAMESPACE{ using MyClass = PRIVATE_NAMESPACE::MyClass; //requires C++11 } mylib/Foo.h #include "priv/Foo.h" namespace PUBLIC_NAMESPACE{ using Foo = PRIVATE_NAMESPACE::Foo; //requires C++11 } Internally you keep everything in a private namespace, so the user is forced to include correct headers immediatly: Now entering the "priv" folder mylib/ priv/ MyClass.h #pragma once #include "Foo.h" namespace PRIVATE_NAMESPACE{ class MyClass{ Foo _foo; public: //... }; } Note how important is the usage of "relative path" inclusion mylib/ priv/ MyClass.cpp #include "MyClass.h" // a implementation detail namespace PRIVATE_NAMESPACE{ //... } mylib/ priv/ Foo.h #pragma once namespace PRIVATE_NAMESPACE{ class Foo{ //... }; } Apart from renaming namespaces, there are no major changes in the pre-existing code nor pre-processor magic, the whole task could be automated so that you get C#-style headers almost for free. Basically you can continue to develop as always because it is always possible to re-import stuff in a different namespace (even third party libraries).

    Effects on client code:

    Without forwarding: #include using namespace PUBLIC_NAMESPACE; int main(){ MyClass a; Foo b; //allowed (public namespace polluted) } With forwarding: #include using namespace PUBLIC_NAMESPACE; int main(){ MyClass a; Foo b; //NOT ALLOWED Compile error (need to include Foo) } Pros
    • Less pollution in public namespace
    • Users are forced to not rely on implementation details
    • Less chance to break code after library refactoring
    Cons
    • Increased compile time
    • More maintenance cost for library developers

    Article updates

    14/11/2015 17:10 added usage example



      Report Article
    Sign in to follow this  


    User Feedback


     

    Thankfully, that'll change in just a few years.

     

     

    You mean C++17 modules right? :) can't wait for that.

    Share this comment


    Link to comment
    Share on other sites
    I personally find your two cons to be actually bigger problems than the pros. I never had much problems with public namespace pollution. I had problems with some code not compiling because I changed what was included in an header file, but if some code break because it relied on some implementation details then that code is buggy. I don't think it is a library fault. It is also a very easy problem to solve.

    Compile time is instead a very big problem in C++ for me. You should really try to reduce it or it will skyrocket when the project grow bigger. You also spend a lot more time maintaining the code than writing it.

    Share this comment


    Link to comment
    Share on other sites

    The problem is that you should never have a using declaration in a header. These should only be in cpp files. Use fully qualified namespace names in your headers. Looks bad at first but eventually it will be normal.

    Share this comment


    Link to comment
    Share on other sites
    I'd also argue that including non-template classes in header files is often a warning sign of problems ahead. Wherever possible, forward declare classes and include them only from the .cpp file. Use the PIMPL idiom where necessary to accomplish this.

    Your compile times will decrease dramatically, and you'll be leaking far less of your class internals into their public API.

    Share this comment


    Link to comment
    Share on other sites

    You mean C++17 modules right? smile.png can't wait for that.

     

    Yep, except it'll be in a technical report rather than in C++17.

    We'll have access to it in 2017, but it'll be alongside C++17, not inside of it.

     

    It'll (likely) be officially included within C++21, and unofficially included alongside C++17, so it can be tweaked and refined while we as a community test it out, so any design problems can be fixed.

    Share this comment


    Link to comment
    Share on other sites

    The problem is that you should never have a using declaration in a header. These should only be in cpp files. Use fully qualified namespace names in your headers. Looks bad at first but eventually it will be normal.

    Ouch, that's not the same at all. Something like "using namespace std" is really a mess and is potentially able to pollute namespace and causing very much problems, in my case I'm just exporting 1 name for header (or if it makes sense, just a bunch of names) wich is very different from importing a whole namespace.
     

    namespace myPublic{
        using K = myPrivate::K;  // results in myPublic::K name => same as declaring K inside "myPublic"
    }
    

    Share this comment


    Link to comment
    Share on other sites

    The problem is that you should never have a using declaration in a header. These should only be in cpp files. Use fully qualified namespace names in your headers. Looks bad at first but eventually it will be normal.

    Even in CPP files they should only be used inside of a scope, you never want to pollute your global scope.

    Share this comment


    Link to comment
    Share on other sites

     

    The problem is that you should never have a using declaration in a header. These should only be in cpp files. Use fully qualified namespace names in your headers. Looks bad at first but eventually it will be normal.

    Ouch, that's not the same at all. Something like "using namespace std" is really a mess and is potentially able to pollute namespace and causing very much problems, in my case I'm just exporting 1 name for header (or if it makes sense, just a bunch of names) wich is very different from importing a whole namespace.
     

    namespace myPublic{
        using K = myPrivate::K;  // results in myPublic::K name => same as declaring K inside "myPublic"
    }
    

    Yes you're correct. I noticed this after I posted and thought it might make it under the radar :) I used to do this sort of thing to make short hands for stuff like the following but I stopped this practice.

    using dmath = dwarf::math;

    Still there is some truth here and it's really nitpicking but I still wouldn't do it. Headers get included in different orders all the time and having code that compiles two different ways based on include order can be hard to find. Namespaces are designed to prevent this sort of thing and making shorthand versions is really just circumventing the system.

     

    Sure the compiler should point out ambiguity for you if there is a problem but it doesn't protect you from old C headers with macros. I would also say the code is harder to read now. Don't spend time on stuff like this as it only takes away from finishing a project.

    Share this comment


    Link to comment
    Share on other sites

     

    Yes you're correct. I noticed this after I posted and thought it might make it under the radar smile.png I used to do this sort of thing to make short hands for stuff like the following but I stopped this practice.

    using dmath = dwarf::math;

    Still there is some truth here and it's really nitpicking but I still wouldn't do it. Headers get included in different orders all the time and having code that compiles two different ways based on include order can be hard to find. Namespaces are designed to prevent this sort of thing and making shorthand versions is really just circumventing the system.

     

    Sure the compiler should point out ambiguity for you if there is a problem but it doesn't protect you from old C headers with macros. I would also say the code is harder to read now. Don't spend time on stuff like this as it only takes away from finishing a project.

     

    Maybe I exposed that bad, it is not a shorthand at all. Differently from the "typical use" of creating short-hands, I'm here proposing a good(at least according to my opinion:P) use of "using directive".

     

    There are no problems of include order etc. and there is no stuff like 2 different versions. Semantically is exactly equivalent to declaring a class with no side-included headers (go back and read sentence again).   I know it is a subtle difference and hard to notice at a first and at a second read.

    Share this comment


    Link to comment
    Share on other sites

     

     

    Yes you're correct. I noticed this after I posted and thought it might make it under the radar smile.png I used to do this sort of thing to make short hands for stuff like the following but I stopped this practice.

    using dmath = dwarf::math;

    Still there is some truth here and it's really nitpicking but I still wouldn't do it. Headers get included in different orders all the time and having code that compiles two different ways based on include order can be hard to find. Namespaces are designed to prevent this sort of thing and making shorthand versions is really just circumventing the system.

     

    Sure the compiler should point out ambiguity for you if there is a problem but it doesn't protect you from old C headers with macros. I would also say the code is harder to read now. Don't spend time on stuff like this as it only takes away from finishing a project.

     

    Maybe I exposed that bad, it is not a shorthand at all. Differently from the "typical use" of creating short-hands, I'm here proposing a good(at least according to my opinion:P) use of "using directive".

     

    There are no problems of include order etc. and there is no stuff like 2 different versions. Semantically is exactly equivalent to declaring a class with no side-included headers (go back and read sentence again).   I know it is a subtle difference and hard to notice at a first and at a second read.

     

     

    I took a better look at what you are doing (waiting for a build to finish). While my initial comments are true they don't fit the scenario you are talking about, as you pointed out. Sorry. I made a poor assumption that you were trying to solve a different problem. Your solution is actually still looking for a problem and it pains me that someone would spend time on this.

     

    I would strongly suggest reading what the master has to say about it in Large Scale C++ Software Design by Lakos. A dry piece of text but the guy knows exactly what he is talking about and I got a lot out of this book.

     

    The issue is that your header should include everything it needs to compile itself and nothing more. Forward references whenever possible. If it doesn't I would pretty much say it's an error in waiting. Also the cpp that includes it should include it's header first to enforce this principle.

     

    The insanity of having two header files with the same name makes me cringe. Anyone trying to read your code or yourself in 6 months is in for a world of hurt...

    Share this comment


    Link to comment
    Share on other sites


    Create an account or sign in to comment

    You need to be a member in order to leave a comment

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!