3
votes
\$\begingroup\$

It always bugged me that you would have to write your private class members inside the header files in C++. This means that everytime you change you implementation, client code has to be rebuild.

I found thise site which describes how to hide implementation detail in C++: http://www.octopull.demon.co.uk/c++/implementation_hiding.html

However I also wanted the code to be generic, so that I didn't have to rewrite the same code for each and every class I create. Here is what I got:

#define TL_UTILS_IMPL_DECL(member) \
    private: \
        class impl; \
        impl* member;

#define TL_UTILS_IMPL_START(t_class, parent) \
    class t_class::impl \
    { \
        public: \
            t_class* parent; \
            impl(t_class* self) \
                : parent(self) \
            {}

#define TL_UTILS_IMPL_END() \
    };

#define TL_UTILS_IMPL_INIT(member) \
    member = new impl(this);

#define TL_UTILS_IMPL_TERM(member) \
    delete member;

#define TL_UTILS_IMPL_PREFIX(t_class) \
    t_class::impl::

Example usage:

// Header file:
    class foo
    {
            TL_UTILS_IMPL_DECL(m)

        public:
            foo();
            ~foo();
    };

// Source file:
    TL_UTILS_IMPL_START(foo, p)
        void bar();
    TL_UTILS_IMPL_END()

    foo::foo()
    {
        TL_UTILS_IMPL_INIT(m)

        // from here on we can use the private
        // data and methods stored in m
        m->bar();
    }

    foo::~for()
    {
        TL_UTILS_IMPL_TERM(m)
    }

    void TL_UTILS_IMPL_PREFIX(foo) bar()
    {
        // do something useful
        // use p as a pointer to the parent class instance
    }

I always hear that preprocessor macros are dirty and should not be used in C++, and I agree to some degree. However I could not realize what I wanted using templates.

So what do you think?

  • Is the use of preprocessor macros justified here?
  • Is the code safe?
  • Any improvements you can think of?
\$\endgroup\$
  • \$\begingroup\$ What you are re-inventing here in PIMPL. \$\endgroup\$ – Martin York Oct 4 '11 at 15:49
  • \$\begingroup\$ I agree with @Tux-D, and my answer reflects some of the drawbacks, referenced here: c2.com/cgi/wiki?PimplIdiom \$\endgroup\$ – Elpezmuerto Oct 4 '11 at 16:14
  • \$\begingroup\$ @Tux-D: Yes ofcourse this is PIMPL, and I didn't invent it as all as stated. I just wanted to know thought about my implementation. \$\endgroup\$ – Lukas Schmelzeisen Oct 4 '11 at 16:42
2
votes
\$\begingroup\$

I certainly sympathize with you about increasing build times. The C++ project I currently work on takes 45 - 60 minutes to build from scratch. But those macros are unreadable. When I see them my eyes glaze over and I don't even want to try to understand what you're doing.

void TL_UTILS_IMPL_PREFIX(foo) bar()

This line in particular is obnoxious. It looks ugly, it's not at all initially clear why you would do this, and once a developer does figure it out, it'll probably make them angry! =)

Since your concern is compile times, let me suggest a set of strategies that I use to alleviate your problem.

  1. Divide your application into a number of small, focused DLL's.
  2. Don't expose concrete classes from you DLL. Instead, have your concrete classes implement interfaces, and compile against these interfaces from other DLL's/EXE's.

Check out this article from CodeProject. Under the heading, Mature Approach: Using an Abstract Interface, for a thorough explanation of point 2.

The end result of these two changes is that all of your DLL's will be able to compile independently of each other. If you keep them small and focused, the cost of recompiling a couple files inside the DLL's isn't too high.

Otherwise, if you really want to use PIMPL, just write the code. Don't bury it in ugly macros.

\$\endgroup\$
  • \$\begingroup\$ +1 Nice article there. I remember reading that some time ago, but some it didn't stick. \$\endgroup\$ – Lukas Schmelzeisen Oct 6 '11 at 17:34
3
votes
\$\begingroup\$

Is the use of preprocessor macros justified here?

To me it seems like overkill. The only non-trivial macro is TL_UTILS_IMPL_START, and that will almost certainly never be useful - usually, the impl class won't need a back-pointer to its wrapper class, but will need other constructor arguments, so you'll usually have to write your own class-specific constructor and initialiser anyway. The other macros are just as verbose as, and much less self-explanatory than, the code they replace.

Is the code safe?

You need to apply the "Rule of Three" - either implement, or remove, the copy constructor and copy assignment operator. I generally use boost::scoped_ptr for the implementation pointer, which sorts this out for me (as well as removing the need for a destructor body).

\$\endgroup\$
  • \$\begingroup\$ +1: Agree. But rather than the adjective overkill I would have used Obfuscation \$\endgroup\$ – Martin York Oct 4 '11 at 17:32
0
votes
\$\begingroup\$

It always bugged me that you would have to write your private class members inside the header files in C++.

This is part of good programming practices. While implementing on these macros (and I agree with @MikeSeymour, that it is overkill), you remember what you are thinking now, but a couple months from now you will have no clue what you were thinking. Also what is immediately evident to you, may not be to another person. If another person can't understand your code or has to spend time deciphering it, that just causes headaches and adds to the development time. Unless you have a damn good reason (and being bugged is not one of them), you really should declare private members in the header.

However I also wanted the code to be generic, so that I didn't have to rewrite the same code for each and every class I create.

If you are focused on generic code, interfaces/templates would be more suitable than a class. If you still want to use OOP, considering leveraging polymorphism or inheritance. By using these two OOP features, you are able to maintain a generic class and good programming practice. This will also help keep your code safe and manage scope properly.

\$\endgroup\$
  • \$\begingroup\$ One reason would be that client code has to be recompiled every time the implementation changes. I'm not expierienced in writing big software projects but I would imagine build times increasing alot. \$\endgroup\$ – Lukas Schmelzeisen Oct 4 '11 at 13:48
  • \$\begingroup\$ @LukasSchmelzeisen, only the client code that has been modified should be rebuilt. There is no reason to do a clean build every time. Plus one of the advantages of OOP is that individual classes or a group of classes in a module can be tested/rebuilt independent of the entire project. \$\endgroup\$ – Elpezmuerto Oct 4 '11 at 14:03
  • \$\begingroup\$ If you are using OOP as designed, I wouldn't consider recompiling a good enough reason to break good practices and OOP conventions. Remember the phase: if you are doing something clever, you're probably doing it wrong \$\endgroup\$ – Elpezmuerto Oct 4 '11 at 14:04

Not the answer you're looking for? Browse other questions tagged or ask your own question.