5
\$\begingroup\$

I'm not very experienced in C++, but I am trying to circumvent a kind of 'constructor order' situation, where I have class A wants class B in its constructor, and vice versa. They don't really want eachother, but both implement an interface and need each other. I decided to give object A a future. Is this even remotely good practice in C++11?

I'm thinking that the code below should be fine - the shared_ptr simply serves to make the future passable by value - multiple future objects can represent the same 'future'. Since I am not experienced in C++, I'm not sure of any potential trouble. I think that I don't need to declare any special copy constructor or such, since I don't do any allocation and std::shared_ptr should be copy-constructible automatically.

What do you think?

#ifndef LIB_FUTURE
#define LIB_FUTURE

#include <stdexcept>
#include <memory>

namespace lib { 
    template <typename TValue>
    class future
    {
    public:
      future() : value_ptr(std::make_shared<TValue*>())
      { }

      void set(TValue &value)
      {
        *value_ptr.get() = &value;
      }

      TValue& get()
      {
        assert_valid_access();
        return **value_ptr;
      }

      TValue* operator->()
      {
        assert_valid_access();
        return *value_ptr;
      }

      bool has_value()
      {
        return value_ptr.get() != nullptr;
      }

    private:
      std::shared_ptr<TValue*> value_ptr;

      void assert_valid_access()
      {
        if(!has_value())
          throw std::runtime_error("Attempting to access a non-set value from a future");
      }
    };

  }

#endif

UPDATE The use case I have is a little more complicated than the above though. Here goes:

class view : public log_sink
{
    view(tcp_client &client);
    virtual sink members...
};

class tcp_client
{
    tcp_client(log) //a sink should be provided before this, since it will do some logging.
};

class log
{
  log(log_sink sink); //So this one needs view.
};

int main()
{
     log log(view);
     tcp_client client(log);
     view the_view(client);
}

The above is not possible. View needs client, but client needs log, but log needs access to an interface on view.

Instead, I opted to give log a promise of a sink in the future. Log implements a tiny buffering mechanism, on the off chance that it doesn't have a sink by the time someone wants logging done. When it finally gets a sink, everything works fine. :)

Like this:

int main()
{
  future<log_sink> promise;
  log log(promise);
  tcp_client client(log);
  view the_view(client);

  promise.set(view);
}
\$\endgroup\$
2
\$\begingroup\$

What's your inheritance relationship between A and B? If A is the parent of B, it's bad design if A needs to do ANYTHING with B in the constructor/destructor because B will not be valid until after A's construction.

If you require RAII design, look at lazy resource acquisition to avoid re-acquisition going up the constructor chain (vector).

Why do you want a smart pointer to a pointer. Smart pointers help you when there is no clear custodian of an instance (or shared primitive type), and you want to destroy an instance only when there are no more users of it. The way you are using the smart pointer means that only a memory location of a pointer is freed, not the memory of a type instance. Plus when you construct this class, the pointer held by the 'value_ptr' is random, which is bad.

I'm not sure where you're going with this 'future' class and how it solves your inter-dependance between A and B.

If A and B need a similar member, make them inherit from the same parent class. If you want to hide this 'shared' member or you want a pure-virtual interface, simply declare the interface as well as defining some sort of linking parent.

If A and B need access to the same instance of something, use a singleton that is inherited from a shared parent.

If you do need a shared instance, and you need to free any resources (IE files) held by the singleton, it's better to write an explicit free function. That way you will easily see where it's being cleaned up.

If you mean you want to avoid initializing a resource that's defined in a parent class, until your child class A or B does, perhaps look at making an explicit, polymorph function.

Hope that helps.

\$\endgroup\$
  • \$\begingroup\$ Thanks for your answer! I think I should have been a bit clearer. They don't have any inheritance relationship - only compositional dependence. Please see my edit - hopefully, the less abstract use case is better. I'll give you a chance to update any changes you might feel necessary (to your answer) before commenting further, since you explicitly stated you don't know where I was going with this! Hope it's not too much of a hassle. Just one thing: std::make_shared<ptr>() should call the default constructor, which initializes the ptr to zero! :) \$\endgroup\$ – Max Apr 7 '12 at 8:29
  • \$\begingroup\$ It's no hassle, I do this to challenge myself. Some questions: Why does log need view, aren't you passing it a log_sink? Why can't view be a polymorph (currently private inheritance) of log_sink which provides the interface you need? What things does log_sink depend on? When would log not have access to log_sink methods ("on the off chance")? Is there a load time issue here (IE is code being executed before main)? \$\endgroup\$ – DiscoStu Apr 7 '12 at 12:44
  • \$\begingroup\$ Oh dang, view is publicly inheriting log_sink - so I am using it polymorphically as a implementor of log_sink. log(...) needs view, polymorphically typed as log_sink. tcp_client is asynchronous, so it's possible that a log message is passed before it is 'given' a log_sink by the promise<log_sink>. In that case, the message is buffered. I'll update with the construction code in a minute. \$\endgroup\$ – Max Apr 7 '12 at 13:00
2
\$\begingroup\$

What you're calling a "future" doesn't model the same concept as std::future, so I think "future" is the wrong name for it. What you've got is more like a "place to put the answer once we get around to computing it", a.k.a. a "variable that starts out uninitialized". There's nothing too special about that, and you could implement it with much less boilerplate if you wanted to.

Re your actual code: you wrote

class view : public log_sink {
    view(tcp_client&);
};

class tcp_client {
    tcp_client(log);
};

class log {
    log(log_sink);
};

Now, log's constructor takes a log_sink parameter, but that's clearly incorrect, because log_sink is the base class of a polymorphic class hierarchy. You don't want to pass a polymorphic type by value (because of slicing); you want to pass it by reference or by (smart) pointer.

Then, view's constructor takes a reference to a tcp_client; but does it really make sense to have a view without a tcp_client? Could we (without loss of generality) assert that every view must have a tcp_client, in which case the tcp_client should actually be a fully owned member of view.

class view : public log_sink {
    tcp_client m_client;
    view(log) : m_client(std::move(log)) { ... }
};

class tcp_client {
    tcp_client(log);
};

class log {
    log(std::shared_ptr<log_sink>);  // takes ownership
};

And then finally we collapse the last level:

class log_sink : public std::enable_shared_from_this<log_sink> { };

class view : public log_sink {
    tcp_client m_client;
    view() : m_client(log(shared_from_this())) { ... }
};

class tcp_client {
    tcp_client(log);
};

class log {
    log(std::shared_ptr<log_sink>);  // takes ownership
};

Now we can construct a view, which contains a tcp_client which contains a log that knows (recursively) about this view.

The shared_ptr part is actually irrelevant at this point; you could just do

class log_sink { };

class view : public log_sink {
    tcp_client m_client;
    view() : m_client(log(*this)) { ... }
};

class tcp_client {
    tcp_client(log);
};

class log {
    log(log_sink&);
};

Does that answer your question, or are you asking about some other "level" of the problem? (For example, this doesn't deal with "how do I prevent the log from logging anything to an only semi-constructed view?", and it doesn't deal with the basic question "how do I forward-declare class tcp_client;?".)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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