views:

478

answers:

6

Hi all,

I'm facing problems with the design of a C++ library of mine. It is a library for reading streams that support a feature I haven't found on other "stream" implementations. It is not really important why I've decided to start writing it. The point is I have a stream class that provides two important behaviours through multiple inheritance: shareability and seekability.

Shareable streams are those that have a shareBlock(size_t length) method that returns a new stream that shares resources with its parent stream (e.g. using the same memory block used by parent stream). Seekable streams are those that are.. well, seekable. Through a method seek(), these classes can seek to a given point in the stream. Not all streams of the library are shareable and/or seekable.

A stream class that both provides implementation for seeking and sharing resources inherits interface classes called Seekable and Shareable. That's all good if I know the type of such a stream, but, sometimes, I might want a function to accept as argument a stream that simply fulfills the quality of being seekable and shareable at the same time, regardless of which stream class it actually is. I could do that creating yet another class that inherits both Seekable and Shareable and taking a reference to that type, but then I would have to make my classes that are both seekable and shareable inherit from that class. If more "behavioural classes" like those were to be added, I would need to make several modifications everywhere in the code, soon leading to unmaintainable code. Is there a way to solve this dilemma? If not, then I'm absolutely coming to understand why people are not satisfied by multiple inheritance. It almost does the job, but, just then, it doesn't :D

Any help is appreciated.

-- 2nd edit, preferred problem resolution --

At first I thought Managu's solution would be my preferred one. However, Matthieu M. came with another I preferred over Managu's: to use boost::enable_if<>. I would like to use Managu's solution if BOOST_MPL_ASSERT produced messages weren't so creepy. If there was any way to create instructive compile-time error messages, I would surely do that way. But, as I said, the methods available produce creepy messages. So I prefer the (much) lesser instructive, yet cleaner message produced when boost::enable_if<> conditions are not met.

I've created some macros to ease the task to write template functions that take arguments inheriting select class types, here they go:

// SonettoEnableIfDerivedMacros.h
#ifndef SONETTO_ENABLEIFDERIVEDMACROS_H
#define SONETTO_ENABLEIFDERIVEDMACROS_H

#include <boost/preprocessor/repetition/repeat.hpp>
#include <boost/preprocessor/array/elem.hpp>
#include <boost/mpl/bool.hpp>
#include <boost/mpl/and.hpp>
#include <boost/type_traits/is_base_and_derived.hpp>
#include <boost/utility/enable_if.hpp>

/*
    For each (TemplateArgument,DerivedClassType) preprocessor tuple,
    expand: `boost::is_base_and_derived<DerivedClassType,TemplateArgument>,'
*/
#define SONETTO_ENABLE_IF_DERIVED_EXPAND_CONDITION(z,n,data) \
        boost::is_base_and_derived<BOOST_PP_TUPLE_ELEM(2,1,BOOST_PP_ARRAY_ELEM(n,data)), \
                BOOST_PP_TUPLE_ELEM(2,0,BOOST_PP_ARRAY_ELEM(n,data))>,

/*
    ReturnType: Return type of the function
    DerivationsArray: Boost.Preprocessor array containing tuples in the form
            (TemplateArgument,DerivedClassType) (see
                    SONETTO_ENABLE_IF_DERIVED_EXPAND_CONDITION)

    Expands:
    typename boost::enable_if<
            boost::mpl::and_<
                    boost::is_base_and_derived<DerivedClassType,TemplateArgument>,
                    ...
                    boost::mpl::bool_<true> // Used to nullify trailing comma
            >, ReturnType>::type
*/
#define SONETTO_ENABLE_IF_DERIVED(ReturnType,DerivationsArray) \
        typename boost::enable_if< \
                boost::mpl::and_< \
                        BOOST_PP_REPEAT(BOOST_PP_ARRAY_SIZE(DerivationsArray), \
                            SONETTO_ENABLE_IF_DERIVED_EXPAND_CONDITION,DerivationsArray) \
                        boost::mpl::bool_<true> \
            >, ReturnType>::type

#endif

// main.cpp: Usage example
#include <iostream>
#include "SonettoEnableIfDerivedMacros.h"

class BehaviourA
{
public:
    void behaveLikeA() const { std::cout << "behaveLikeA()\n"; }
};

class BehaviourB
{
public:
    void behaveLikeB() const { std::cout << "behaveLikeB()\n"; }
};

class BehaviourC
{
public:
    void behaveLikeC() const { std::cout << "behaveLikeC()\n"; }
};

class CompoundBehaviourAB : public BehaviourA, public BehaviourB {};
class CompoundBehaviourAC : public BehaviourA, public BehaviourC {};
class SingleBehaviourA : public BehaviourA {};

template <class MustBeAB>
SONETTO_ENABLE_IF_DERIVED(void,(2,((MustBeAB,BehaviourA),(MustBeAB,BehaviourB))))
myFunction(MustBeAB &ab)
{
    ab.behaveLikeA();
    ab.behaveLikeB();
}

int main()
{
    CompoundBehaviourAB ab;
    CompoundBehaviourAC ac;
    SingleBehaviourA    a;

    myFunction(ab); // Ok, prints `behaveLikeA()' and `behaveLikeB()'
    myFunction(ac); // Fails with `error: no matching function for
                    // call to `myFunction(CompoundBehaviourAC&)''
    myFunction(a);  // Fails with `error: no matching function for
                    // call to `myFunction(SingleBehaviourA&)''
}

As you can see, the error messages are exceptionally clean (at least in GCC 3.4.5). But they can be misleading. It doesn't inform you that you've passed the wrong argument type. It informs you that the function doesn't exist (and, in fact, it doesn't due to SFINAE; but that may not be exactly clear to the user). Still, I prefer those clean messages over those randomStuff ... ************** garbage ************** BOOST_MPL_ASSERT produces.

If you find any bugs in this code, please edit and correct them, or post a comment in that regard. The one major issue I find in those macros is that they're limited to some Boost.Preprocessor limits. Here, for example, I can only pass a DerivationsArray of up to 4 items to SONETTO_ENABLE_IF_DERIVED(). I think those limits are configurable though, and maybe they will even be lifted in upcoming C++1x standard, won't they? Please, correct me if I'm wrong. I don't remember if they have suggested changes to the preprocessor.

Thank you.

A: 

You might want the Decorator pattern.

ChrisW
A: 

Assuming both Seekable and Shareable have common ancestor, one way I can think of is trying to downcast (of course, asserts replaced with your error-checking):

void foo(Stream *s) {
    assert(s != NULL);
    assert(dynamic_cast<Seekable*>(s) != NULL);
    assert(dynamic_cast<Shareable*>(s) != NULL);
}
PiotrLegnica
+12  A: 

Just a few thoughts:

STL has this same sort of problem with iterators and functors. The solution there was basically to remove types from the equation all together, document the requirements (as "concepts"), and use what amounts to duck typing. This fits well a policy of compile-time polymorphism.

Perhaps a midground would be to create a template function which statically checks its conditions at instantiation. Here's a sketch (which I don't guarantee will compile).

class shareable {...};
class seekable {...};

template <typename StreamType>
void needs_sharable_and_seekable(const StreamType& stream)
{
    BOOST_STATIC_ASSERT(boost::is_base_and_derived<shareable, StreamType>::value);
    BOOST_STATIC_ASSERT(boost::is_base_and_derived<seekable, StreamType>::value);
    ....
}

Edit: Spent a few minutes making sure things compiled, and "cleaning up" the error messages:

#include <boost/type_traits/is_base_and_derived.hpp>
#include <boost/mpl/assert.hpp>

class shareable {};
class seekable {};

class both : public shareable, public seekable
{
};


template <typename StreamType>
void dosomething(const StreamType& dummy)
{
  BOOST_MPL_ASSERT_MSG((boost::is_base_and_derived<shareable, StreamType>::value),
                       dosomething_requires_shareable_stream, 
                       (StreamType));
  BOOST_MPL_ASSERT_MSG((boost::is_base_and_derived<seekable, StreamType>::value),
                       dosomething_requires_seekable_stream, 
                       (StreamType));
}

int main()
{
  both b;
  shareable s1;
  seekable s2;
  dosomething(b);
  dosomething(s1);
  dosomething(s2);
}
Managu
This sure seems a good way to work that out. I will toy around with some ideas I've had. I'll probably accept your answer when I'm finished and I'll edit my question with the solutions I've found. Thanks.
n2liquid
+1 for the use of the 'msg' variation of the assert, makes compilation error much more readable!
Matthieu M.
+1  A: 

How about using a template method?

template <typename STREAM>
void doSomething(STREAM &stream)
{
  stream.share();
  stream.seek(...);
}
rstevens
Simply using templates can make things really unintuitive. The interface of what is wanted is used, but a novice library user would not notice from which class that interface belonged to. Using `boost::enable_if` or `BOOST_MPL_ASSERT` as suggested does better the job.
n2liquid
But you still have to downcast the stream by using a dynamic_cast<..> because your parameter is still a stream. The template solution is faster because the "cast" is done on compile time. Sure it is not that intuitive.
rstevens
Nope. The `enable_if` solution requires no cast because it also uses templates, just like yours, but additional per-type checking is added. The required types still don't show up in the error message, but people can at least find them in the function signature. Read my edited post and test the sample code for more details, I think it may be worthwhile.
n2liquid
@n2liquid: You're right. After thinking about what I wrote I came to the same: I was just stupid to oversea that it uses templates... So sorry for that
rstevens
+5  A: 

Take a look at boost::enable_if

// Before
template <class Stream>
some_type some_function(const Stream& c);

// After
template <class Stream>
boost::enable_if<
  boost::mpl::and_<
    boost::is_base_and_derived<Shareable,Stream>,
    boost::is_base_and_derived<Seekable,Stream>
  >,
  some_type
>
some_function(const Stream& c);

Thanks to SFINAE this function will only be considered if Stream satisfies the requirement, ie here derive from both Shareable and Seekable.

Matthieu M.
A: 

Replace 'shareable' and 'seekable' with 'in' and 'out' and find your 'io' solution. In a library similar problems should have similar solutions.

ima