4

Here's a simplified example of what is called (I hope - please, correct me if I'm wrong) Strategy pattern: there's a class FileWriter which writes key-value pairs to a file and uses object of IFormatter interface for formatting text being written. There are different formatters implementations and formatter object is passed when FileWriter is created. Here's one (bad) implementation of such pattern:

#include <iostream>
#include <fstream>
#include <stdlib.h>
#include <sstream>

using namespace std;

class IFormatter {
  public:
    virtual string format(string key, double value) = 0;
};

class JsonFormatter : public IFormatter {
  public:
    string format(string key, double value) { 
        stringstream ss;
        ss << "\""+key+"\": " << value;
        return ss.str();
    }
};

class TabFormatter : public IFormatter {
  public:
    string format(string key, double value) { 
        stringstream ss;
        ss << key+"\t" << value;
        return ss.str();
    }
};

class FileWriter {
  public:  
    FileWriter(string fname, IFormatter& fmt):fmt_(fmt)
    {
        f_.open(fname.c_str(), ofstream::out);
    }

    void writePair(string key, double value)
    {
        f_ << fmt_.format(key, value);
    }

  private:
    ofstream f_;
    IFormatter& fmt_;
};    

As can be seen, the main drawback of such approach is it's unreliability - Formatter object passed to FileWriter has to exist during whole FileWriter's lifetime, thus calls like FileWriter("test.txt", JsonFormatter()) lead directly to SegFault.

In this regard, I'd like to discuss what could be the other options for implementing such an approach with "easy-to-use" and simplicity requirements:

  • either new formatter can be passed when file writer is created, or
  • existing formatter can be passed and used.

I came up with several alternatives described below with their drawbacks (IMO):

  • templates: having FileWriter as a template class which takes exact FormatterClass as an argument; drawback: ugly to call: FileWriter<JsonFormatter>("test.txt", JsonFormatter()) - here, JsonFormatter is typed twice.
  • raw pointers: FileWriter("test.txt", new JsonFormatter()); drawback - who should delete formatter object? FileWriter? if yes, then passing an address of existing formatter will lead to SegFault once FileWriter object attempts to delete formatter.
  • shared pointers: FileWriter("test.txt", dynamic_pointer_cast<IFormatter*>(shared_ptr<JsonFormatter*>(new JsonFormatter())); drawback: ugly to call, and again, what if formatter was created before creating file writer?

What would be the best practices here?

UPDATE

In response to answers that suggested to use std::function - What if Formatter may store a state (say, precision) and have additional methods, like getHeader(), for instance, for CSV files?

Additionaly, storing IFormatter by value isn't possible since it's an abstract class.

peetonn
  • 2,942
  • 4
  • 32
  • 49

4 Answers4

2

The simplest solution is to use:

JsonFormatter formatter;
FileWriter writer("test.txt", formatter);
// Use writer.

The other option that is a little bit better is to have a clone() function in IFormatter. Then, FileWriter can clone the object, take ownership of the clone and delete it in its destructor.

 class IFormatter {
   public:
     virtual string format(string key, double value) = 0;
     virtual IFormatter* clone() const = 0;
 };


 class FileWriter {
   public:  

     FileWriter(string fname, IFormatter const& fmt):fmt_(fmt.clone())
     {
         f_.open(fname.c_str(), ofstream::out);
     }

     ~FileWriter()
     {
        delete fmt_;
     }

     void writePair(string key, double value)
     {
         f_ << fmt_->format(key, value);
     }

   private:
     ofstream f_;
     IFormatter* fmt_;
 };    

Now, you can call FileWriter with temporary object too.

FileWriter writer("test.txt", JsonFormatter());
// Use writer.
R Sahu
  • 204,454
  • 14
  • 159
  • 270
  • 1
    while this solution will work, indeed, the design described in question seems to me more like a general approach which should be supported by language/standard library capabilities (or require a different design/pattern) rather than require a programmer to explicitly handle it by writing `clone()` methods for his classes. – peetonn Mar 14 '16 at 01:39
1

templates: having FileWriter as a template class which takes exact FormatterClass as an argument; drawback: ugly to call: FileWriter("test.txt", JsonFormatter()) - here, JsonFormatter is typed twice.

More templates!

template<class Formatter> 
FileWriter<Formatter> makeFileWriter(const std::string& filename, const Formatter& formatter) 
{return FileWriter<Formatter>(filename, formatter);}

Ta da! Now it's as simple as:

auto fileWriter = makeFileWriter("test.txt", JSonFormatter());`
Mooing Duck
  • 64,318
  • 19
  • 100
  • 158
  • thanks for your answer! I'll still try to refrain from using templates for now, unless I'll find a suitable solution. – peetonn Mar 14 '16 at 01:42
1

This is what the standard library does (e.g. std::shared_ptr can take a deleter). Formatter must be copy constructible, and obviously the expression f << fmt(key, value) must be well-formed.

class FileWriter {
public:
    template<typename Formatter>
    FileWriter(std::string fname, Formatter fmt) :
        fmt(fmt)
    {
        f.open(fname.c_str(), std::ofstream::out);
    }

    void writePair(std::string key, double value)
    {
        f << fmt(key, value);
    }

private:
    std::ofstream f;
    std::function<std::string (std::string, double)> fmt;
};

If you need more than one function in your interface, you can use your original approach but control the lifetime of the formatter with std::unique_ptr or std::shared_ptr (remember to make the destructor virtual).

struct Formatter
{
    virtual ~Formatter() {}
    virtual std::string format(std::string key, double value) = 0;
};

class FileWriter {
public:
    FileWriter(std::string fname, std::unique_ptr<Formatter>&& fmt_)
    {
        if (!fmt_)
        {
            throw std::runtime_error("Formatter cannot be null");
        }

        f.open(fname.c_str(), std::ofstream::out);

        fmt = std::move(fmt_); // strong exception safety guarantee
    }

    void writePair(std::string key, double value)
    {
        f << fmt->format(key, value);
    }

private:
    std::ofstream f;
    std::unique_ptr<Formatter> fmt;
};

If you want to pass an existing Formatter to the FileWriter, you either need to copy/move it into a smart pointer to transfer ownership, or you need to wrap it in the formatter interface.

class FormatterProxy : public Formatter
{
public:
    FormatterProxy(Formatter& fmt) :
        fmt(fmt)
    {
    }

    std::string format(std::string key, double value)
    {
      return fmt.format(key, value);
    }

private:
  Formatter& fmt;
};

This still has the lifetime management issue you are trying to avoid. However, I do not see any way around this. Either you give unique or shared ownership of the Formatter to the FileWriter, or you leave lifetime management in the hands the caller (which is a perfectly valid approach if you value efficiency over safety).

Joseph Thomson
  • 9,888
  • 1
  • 34
  • 38
  • same comment as for @Yakk's answer - what if `IFormatter` has to carry a state (say, precision for double) or may have additional methods (like, `getHeader()` for CSV files formatting)? Should there be a different design? – peetonn Mar 14 '16 at 01:36
  • 1
    As long as `Formatter` is copy constructible, there is no reason it cannot carry state. If you need additional methods, you could either pass a formatter which implements `operator()`, or wrap your formatter in a lambda: `[formatter](std::string s, double d) { return formatter.format(s, d); }` (here, either make `format` `const` or the lambda `mutable`). – Joseph Thomson Mar 14 '16 at 01:49
  • @peetonn I think I know what you mean w.r.t. extra methods. I have updated my answer. Let me know if it helps. – Joseph Thomson Mar 14 '16 at 05:18
0
using IFormatter - std::function<std::string(std::string,double)>;

Your formatter should be a function, not an interface.

Callers can use std::ref if they want to guarantee lifetime, wrap a shared ptr if they want nebulous lifetime, or pass by-value.

If you want a richer interface, you can either take a pile of such, or write a class that is a pile of such (either via inheritance or by writing notstd::functions manually).

Store IFormatter fmt; by-value, use fmt(a,b) instead of fmt.format(a,b) (DRY!). Client code can make it ref or smart semantics if it wants.

inheritance as an implementation detail, instead of driving your design, is freeing.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • why it should be a function? what if formatter should save some state, say, precision for doubles which is specified during formatter construction? – peetonn Mar 12 '16 at 08:28
  • @peet std function can do that. – Yakk - Adam Nevraumont Mar 12 '16 at 12:20
  • storing `IFromatter` by value isn't possible - as it's an abstract class. regarding using `std::function` I don't think it applies to this case - formatter can have other methods as well (in my more complex case, formatter has method `getHeader()` which returns a header for CSV files for instance). – peetonn Mar 14 '16 at 01:29
  • @peetonn My `IFormatter` works by value. Yours does not. That is a problem with your solution, hence your lifetime management headaches. If you want to get rid of lifetime management headaches baked into your solution, stop baking pointers into your interfaces: pointers (and references) *imply* lifetime management headaches. Your actual use case, not reflected in the OP, would require a bit more work to use type erasure on, but it is quite doable. Your belief that `std::function` cannot store state is ... incorrect. Generalizations of `std::function` solve your general problem. – Yakk - Adam Nevraumont Mar 14 '16 at 02:02