
Generic programming to fight the rigidity in the C++ projects - cpp86
http://www.codergears.com/Blog/?p=945
======
zmmmmm
I find the whole focus on abstraction as a solution to flexibility very
troublesome. Taking the declaration for a C++ function and whacking an
abstract interface only goes a small way to making the system more flexible.
All your interacting components still depend intimately on each other's
_behavior_. The behavior is not fully captured by the interface - if it was,
it wouldn't be an interface, it would be an implementation. So abstractions
leak by their very nature, and the success of our attempt to enable
flexibility through those interfaces depends completely on how well we choose
what the interfaces are and how much of the behavior specification we decide
should live in the interface definition vs the implementation. That subtlety
of the design process is completely lost by this love of abstraction where it
is assumed that anything with an abstract interface is completely flexible
whereas anything without one is not.

~~~
cbsmith
> The behavior is not fully captured by the interface - if it was, it wouldn't
> be an interface, it would be an implementation.

I think your argument is weakened by the reality that even "behaviour" has
plenty of abstraction in it... particularly if you are using a compiler.
You've got the compiler, library linkages (and to different libraries), the
OS's abstraction, and of course these days there's usually a hypervisor in
there. Then there's the abstractions in the hardware... You just can't get
away from it.

Good interfaces allow encapsulation, which tends to be very helpful, although
there is a point where it does more harm than good.

> That subtlety of the design process is completely lost by this love of
> abstraction where it is assumed that anything with an abstract interface is
> completely flexible whereas anything without one is not.

It's all abstract. Design is concerned about the _right_ abstraction. The
wrong abstraction gives you no flexibility, or no flexibility where you need
it, or just a ton of unneeded complexity. Abstraction for abstraction's sake
suggests a _lack_ of design.

~~~
userbinator
> Abstraction for abstraction's sake suggests a _lack_ of design.

And yet a surprisingly large number of people think "good" design is entirely
about adding abstractions until no more can be added. The classic example of
this is enterprise Java, where designs are almost always overly abstract and
flexible, but only in these narrow directions that were thought to be where
_extreme_ flexibility will be required in the future; and inevitably, the
flexibility that's eventually needed turns out to be in a completely different
direction.

~~~
contravariant
In their defense mathematicians have gained huge advances in algebra (and
other areas) precisely by abstracting away all unnecessary parts (most notably
by Emmy Noether).

Of course working out which parts are necessary and which aren't is a very
subtle process, and one of the areas of programming where a mathematical
background can be a huge advantage.

~~~
cbsmith
You nailed it. Getting the _right_ abstraction is very hard. One does one's
best, and generally fails miserably.

------
humanrebar
This is an interesting write-up, but it leaves out some specifics in designing
C++. A few are:

1\. Concurrency

In garbage collected languages, the runtime can generally add objects to a
free list and clean up memory when it's more-or-less convenient. Since C++
uses deterministic destruction and freeing, there has to be clear
responsibilities defined. In particular, it has to be clear which thread is
ultimately responsible for running the destructor. Point being, writing code
that would otherwise be "abstract" in Java-land implies (but rarely explicitly
defines!) some rigidity in design with respect to how long objects need to be
around for different threads to use them.

Maybe that's too abstract. Here's some code to illustrate:

    
    
        class IHandler {
        public:
            virtual Status handleAsync(const Msg & msg) = 0
        };
    
        class CopyingHandler : public IHandler {
        private:
            WorkerThread m_worker;
    
        public:
            Status handleAsync(const Msg & msg) override {
                // capture the msg by value, taking a copy
                return m_worker.push_back([=msg](){
                    handle(msg);
                });
            }
        };
    
        class RefHandler : public IHandler {
        private:
            WorkerThread m_worker;
    
        public:
            Status handleAsync(const Msg & msg) override {
                // Capture the msg by reference. Assumes
                // sticks around at least as long as the
                // background thread does.
                return m_worker.push_back([&msg](){
                    handle(msg);
                });
            }
        };
    

Anyway, I left some details out, but you can see that both implementations
adhere to the interface provided by IHandler, but each makes drastically
different assumptions about what's OK. In some cases, problem domains, copying
the message might create a performance bug. In most programs, you could easily
(likely) have undefined behavior by capturing the message by reference,
letting the message be destroyed, and then trying to use the message
afterwards.

Point being, rigidity in C++ designs is easy to create subtly and
accidentally, and even generic programming won't necessarily help you here.

2\. Polymorphism

C++ actually allows for quite a range of polymorphic behaviors. You can't
really rank them from highest rigidity to the least, since there are pros and
cons to each approach, but generally inheritance is more rigid than the other
approaches. In contrast, you can also have:

2a. Opaque Pointers

Not necessarily very type safe, but if you need to wrap something up in a
black box, pass it around, then unwrap it and use it later (maybe in a message
queue, a mailbox, a private implementation, or in certain kinds of IOC
patterns), you can completely lose your type and recover it later. Typically
there is a small performance hit as you use RTTI
([https://en.wikipedia.org/wiki/Run-
time_type_information](https://en.wikipedia.org/wiki/Run-
time_type_information)) or a discriminator (an enum maybe) to decide how to
recover your type when you're ready for it. See <experimental/any>
([http://en.cppreference.com/w/cpp/experimental/any](http://en.cppreference.com/w/cpp/experimental/any))
for an example of this.

2b. Type Erasure

This one is more involved but clever use of generic programming lets us
automatically provide the adaptor boilerplate to get types to play nice with
other code. Sean Parent does an excellent walkthrough of this technique in his
GoingNative talk in 2013
([https://channel9.msdn.com/Events/GoingNative/2013/Inheritanc...](https://channel9.msdn.com/Events/GoingNative/2013/Inheritance-
Is-The-Base-Class-of-Evil)). Point being that type erasure lets you have a
different flavor of flexibility in a way not seen in Java-land.

2c. Unions

This is harder to do properly in practice, as evidenced by the difficulty in
standardizing something like boost::variant, but from time to time it might be
worth playing around with various types of unions. They're a bit awkward, but
you might try boost::variant if you like the tradeoffs it provides.

3\. Value Semantics

Since objects are values in C++, you need to know the size of things much more
often. This causes dependencies you might not think about in other languages,
depending on how their standard libraries are designed.

~~~
catnaroek
> Since C++ uses deterministic destruction and freeing, there has to be clear
> responsibilities defined. In particular, it has to be clear which thread is
> ultimately responsible for running the destructor.

Ownership itself is an abstraction that is very useful for manipulating
ephemeral resources, and which is very difficult to express in languages where
garbage collection is the mandated universal solution for resource
reclamation.

> Point being, writing code that would otherwise be "abstract" in Java-land
> implies (but rarely explicitly defines!) some rigidity in design with
> respect to how long objects need to be around for different threads to use
> them.

When you care about ownership, “who frees this object” isn't an implementation
detail or an abstraction leak. It's a part of the interface.

> Since objects are values in C++, you need to know the size of things much
> more often.

Pretty sure everything is a value in ML and Haskell, and I don't recall every
having to worry about the size of anything when using those languages. C++
programmers tend to wrongly conflate “value” with “a physical copy of the
value”. It's better than what you get in Java (no user-defined values at
all!), but it still leaves a lot to be desired.

~~~
humanrebar
> Pretty sure everything is a value in ML and Haskell,

That's a good point. I guess it's the combination of value semantics and
manual memory management. I'll try to keep that in mind in the future.

> When you care about ownership, “who frees this object” isn't an
> implementation detail or an abstraction leak. It's a part of the interface.

Exactly. But even more than that, references that don't extend the lifetimes
of objects (like native pointers and references) are simple to create and are
in many ways the default type of reference in C++.

At any rate, 'this message queue _only_ works with types that are easy to
copy' is not generally a prominent part of the documentation in C++, if it's
present at all.

~~~
cbsmith
> But even more than that, references that don't extend the lifetimes of
> objects (like native pointers and references) are simple to create and are
> in many ways the default type of reference in C++.

C++ has a lot of cases of defaults that one tends not to use all that much
(because it tends to default to maximal efficiency). Raw pointers would be a
good example. Those are generally frowned upon.

On the other hand, references are encouraged and have pretty clear semantics
around ownership.

> At any rate, 'this message queue only works with types that are easy to
> copy' is not generally a prominent part of the documentation in C++, if it's
> present at all.

Really? That's a very prominent part of say... the STL's documentation.

~~~
humanrebar
Even the documentation of vector describes what it does and few implications.
It doesn't warn you that vectors of raw pointers are bad parameter types, for
example. It does talk about iterator invalidation, but it doesn't really
educate us about how that affects our designs.

That's not to criticize vector, but to point out that C++ had especially
nuanced implications if loosely coupled design is a concern.

~~~
cbsmith
> It doesn't warn you that vectors of raw pointers are bad parameter types,
> for example.

Vectors of raw pointers function very effectively, they just have all the
problems intrinsic with the use of raw pointers. I'm not sure I'd blame the
vector documentation for that...

> It does talk about iterator invalidation, but it doesn't really educate us
> about how that affects our designs.

Design implications are a subtle thing that you can write entire books about.
I think failing to fully capture those implications in the API documentation
is more than understandable and far from unusual. For example, Java's
containers have similar issues with iterator invalidation, and similarly don't
have documentation on all the design implications.

