It is true that you must copy objects if you intend to store them as members in your class.
Saving links is dangerous because you can pass temporary objects as parameters to your ctor, which will lead to broken links when temporary files are destroyed.
Passing parameters as pointers is an alternative, but this aproach is also problematic, since then it becomes possible to pass a nullptr value (NULL value), and you should check this.
Another alternative would be to move the values, that is, pass parameters as references to the r-value. This will avoid copying, however, the client will need to either pass temporary or std::move objects when ctor is called. It would be impossible to pass references to the l-value.
// Define ctor as taking r-value references. template <class Filter, class Formatter, class Outputter> LoggerImpl<Filter, Formatter, Outputter>::LoggerImpl(Filter&& filter, Formatter&& formatter, Outputter&& outputter) : mFilter(std::move(filter)), mFormatter(std::move(formatter)), mOutputter(std::move(outputter)) { // ... } /* ... */ // Calling ctor. FileLogger f1(NoFilter(), SimpleFormatter(), FileOutputter("log.txt")); // OK, temporaries. FileOutputter fout("log.txt"); FileLogger f2(NoFilter(), SimpleFormatter(), fout); // Illegal, fout is l-value. FileLogger f3(NoFilter(), SimpleFormatter(), std::move(fout)); // OK, passing r-value. fout may not be used after this!
If you decide to go with a copying approach, I recommend instead passing your parameters by value to ctor. This will allow the compiler to perform optimization as copy elision (read: Want Speed? Pass by Value ).
template <class Filter, class Formatter, class Outputter> LoggerImpl<Filter, Formatter, Outputter>::LoggerImpl(Filter filter, Formatter formatter, Outputter outputter) : mFilter(std::move(filter)), mFormatter(std::move(formatter)), mOutputter(std::move(outputter)) {
Using the definition above: at best, the compiler will return a copy, and the members will be moved along the path (when passing a temporary object).
In the worst case: a copy and a move construct will be executed (when passing an l-value).
Using your version (passing parameters as a reference to const), a copy will always be executed, since the compiler cannot perform optimizations.
To create a construction for work , you need to make sure that types that are passed as parameters are moved constructively (either implicitly or using the declared ctor move). If the type does not move constructively, it will be copied.
When it comes to copying a stream to FileOutputter , using std::shared_ptr seems like a good solution, although you should initialize mStream in the initialization list instead of assigning it to the ctor package:
explicit FileOutputter(const char* fname) : mStream(std::make_shared<std::ofstream>(fname)) {} // Note: Use std::ofstream for writing (it has the out-flag enabled by default). // There is another flag that may be of interest: std::ios::app that forces // all output to be appended at the end of the file. Without this, the file // will be cleared of all contents when it is opened.
A std::ofstream not copied and runs around a smart pointer (be sure to use std::shared_ptr though), is probably the easiest solution in your case, and also, in my opinion, contrary to what you say, not too complicated.
Another approach would be to make the stream element static, but then each FileOutputter instance will use the same std::ofstream , and it would be impossible to use parallel logger objects to write to different files, etc.
Alternatively, you can move the stream, since std::ofstream not copied, but moved. However, this would require making FileOutputter movable and not copied (and probably LoggerImpl ), since using a "moved" object other than its dtor can lead to UB. Creating an object that controls only the types of movement becomes only a movement, but sometimes it can make a lot of sense.
std::ofstream out{"log.txt"}; std::ofstream out2{std::move(out)}
In addition, in the above example, you do not need to declare a copy of ctor or dtor for FileOutputter , since they will be implicitly generated by the compiler.