Passing around heterogenous data

This is more a question about general design than specifically about C++.
I have a class Consumer that needs to receive a sequence<A>, a sequence<B>, and a C, where A, B, and C are all unrelated. Consumer has to write these objects to a file in a specific order, and it's incorrect for the objects to be reordered or for the sequences to be overlapped (e.g. [A[0], A[1], B[0], A[2], B[1], B[2]]).
I'm trying to figure out some way that neither the producer nor the consumer have to assume correct behavior from the other party, but also to obviate error checking. In the code I'm translating, I had solved it like this:
1
2
3
4
5
6
class Consumer{
public:
    void add_A(const A &);
    void add_B(const B &);
    void add_C(const C &);
};
And each function would set a state variable the first time it was called and check it every time to make sure it wasn't in an incorrect state. This works but it's not very nice.
Now I was going to use something more like this:
1
2
3
4
class Consumer{
public:
    void add(coroutine &, coroutine &, const C &);
};
But I realized this just moves the error checking elsewhere, since extracting from the coroutines in the wrong order is still an error, because extracting changes the state of the other objects.

Can anyone think of an elegant solution for this?
(This may be naive or you might have already thought of this but...)

Can the consumer create Qs for A, B, C and then write them at the end of input from producer?
One drawback is this will occupy lot of memory for large data, but it doesn't need a state machine and/or flags. This can be easily made to handle 'D' in case it gets added in future as well
That doesn't work, because the action of writing an A to a file changes the future state of some B that will be added later, as well as of the C. This state change must happen at the moment the file is written to avoid redundant I/O operations.
Last edited on
I solved this ordering problem with a key:

The data is stored with a key. There is an additional array/container that contains the order of the keys. When you want to store the data use the keys of that container and fetch/store the data according their key.
Where relevant, the order of the objects within their implied sequences is implicitly given by the order in which the respective data structures that contain them will be traversed, which is well-defined. The problem is ensuring that:
* All As will be written before any Bs.
* All Bs will be written before the one C.
* Correct behavior from the other party is not assumed.
* Error checking is minimized.

It's increasingly starting to look like this is one of those extremely few instances where dynamic typing is of any use.
So when do you need to write this data out? As soon as it is sent or are you waiting until you get some sort of signal to write the data?

Also, will you have all types always? Or can you get a bunch of As then a C and be good to write?
No, there's no asynchronicity. The existing code writes them inside the corresponding Consumer::add_*().

Also, will you have all types always? Or can you get a bunch of As then a C and be good to write?
Could you be more explicit?
Could the producer do something like:

add_a
add_a
//no add_bs
add_c
write_data


Also, is the order of the add_a/b/c important? i.e. Is
add_a(a1)
add_b(b1)
add_a(a2)
add_c(c)
--- Output: ---
a1
a2
b1
c


Valid? Or should it be:
add_a(a1)
add_a(a2)
add_b(b1)
add_c(c)
--- Output: ---
a1
a2
b1
c
No, there will always be at least one B added, but there are situations where no As are added.

No. add_a(a1); add_b(b1); add_a(a2); would be an error because add_a(a2) modifies the state of b1.
Well, thanks everyone, but I found an acceptable solution using a helper class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class Consumer_helper{
public:
    typedef const A *first_push_t;
    typedef const B *second_push_t;
    typedef const C *third_push_t;
    typedef boost::coroutines::asymmetric_coroutine<first_push_t> first_co_t;
    typedef boost::coroutines::asymmetric_coroutine<second_push_t> second_co_t;
    typedef boost::coroutines::asymmetric_coroutine<third_push_t> third_co_t;

    virtual ~Consumer_helper(){}

    virtual std::shared_ptr<first_co_t::pull_type> first(){
        throw std::exception("Incorrect use");
    }
    virtual std::shared_ptr<second_co_t::pull_type> second(){
        throw std::exception("Incorrect use");
    }
    virtual std::shared_ptr<third_co_t::pull_type> third(){
        throw std::exception("Incorrect use");
    }
};

#define DERIVE_Consumer_helper(x)                    \
class Consumer_helper_##x : public Consumer_helper{  \
    typedef std::shared_ptr<x##_co_t::pull_type> r;  \
    r data;                                          \
public:                                              \
    Consumer_helper_##x(const r &data): data(data){} \
    r x() override{                                  \
        return this->data;                           \
    }                                                \
}

DERIVE_Consumer_helper(first);
DERIVE_Consumer_helper(second);
DERIVE_Consumer_helper(third);

class Consumer{
    void add_A(const A &);
    void add_B(const B &);
    void add_C(const C &);
public:
    void process(Consumer_helper *begin, Consumer_helper *end);
};

void Consumer::process(Consumer_helper *begin, Consumer_helper *end){
    if (end - begin != 3)
        throw std::exception("Incorrect usage");

    {
        auto co = begin->first();
        for (auto i : *co)
            this->add_A(*i);
    }
    {
        auto co = begin->second();
        for (auto i : *co)
            this->add_B(*i);
    }
    {
        auto co = begin->third();
        for (auto i : *co){
            this->add_C(*i);
            break;
        }
    }
}
Maybe you could try also using proxy objects? Something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ConsumerA {
public:
    ConsumerA& add(const a&);
    ConsumerB& add(const b&);
};

ConsumerB {
public:
    ConsumerB2& add(const b&);
};

ConsumerB2 {
public:
    ConsumerB2& add(const b&);
    ConsumerC& add(const c&);
};

ConsumerC {
};


This requires you to hardcode all the states however, so it's hard to modify if you might change them. It might look nicer on the user's end depending on how exactly they are using it.
Isn't this just a state machine?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Consumer {
public:
    Consumer() : accepting(A) {}
    add(const A& a) {
        // Must be accepting A
        if (accepting != A) error();
    }

    add(const (B& n) {
        // Can be accepting A or B, but not C
        if (accepting == C) error();
        accepting = B;
    }
    add(const C& c) {
        if (accepting == A) error();
        accepting = C;
    }
private:
    enum { A, B, C} accepting;
}

It's more like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Consumer {
public:
    Consumer() : state(Initial) {}
    add(const A& a) {
        if (state == Initial)
            //do something
        else if (state == A)
            //do some other thing
        else
            error();
        state = A;
        //etc.
    }

    add(const (B& n) {
        if (state == A)
            //do something
        else if (state == B)
            //do some other thing
        else
            error();
        state = B;
        //etc.
    }
    add(const C& c) {
        if (state != B)
            error();
        //etc.
    }
private:
    enum { Initial, A, B, C} state;
}
This solution is cleaner.
Last edited on
Topic archived. No new replies allowed.