1

I want to take input from an istream from either a std::ifstream or std::cin depending on some condition.

As far as I can get it working, I had to use a raw std::istream* pointer:

int main(int argc, char const* argv[]) {
    std::istream *in_stream;
    std::ifstream file;
    if (argc > 1) {
        std::string filename = argv[1];
        file.open(filename);
        if (not file.is_open()) {
            return -1;
        }
        in_stream = &file;
    } else {
        in_stream = &std::cin;
    }
    // do stuff with the input, regardless of the source...
}

Is there a way to rewrite the above using smart pointers?

bigyihsuan
  • 172
  • 1
  • 9
  • A smart pointer does not make sense in this context. `file`lives until this function exits. You use smart pointers only when you use dynamic memory. But thats not the case here. – Raildex Jun 23 '22 at 05:54
  • Why do you want a smart pointer? Are you trying to make sure that `std::cin` is destroyed at some point? – JaMiT Jun 23 '22 at 05:57
  • 6
    I would write a function `do_stuff(std::istream& input)` and then pass one of the streams to it. No need for pointers. – BoP Jun 23 '22 at 08:55
  • All these `istream`-related classes actually already are smart pointers! The object they manage is an instance of some class derived from `streambuf`. – Ben Voigt Jun 23 '22 at 17:00

2 Answers2

1

This may be a question about ownership: How does one wrap and pass around two different std::istream instances, one of which is owned and needs disposal (which calls close() upon destruction) and ownership transfers, whereas the other one is not owned by user code (e.g. std::cin) and cannot be deleted. The solution is polymorphism: Create an interface like

struct StreamWrapper {
  virtual std::istream &stream() = 0;
  virtual ~StreamWrapper() = default;
};

and two implementations, e.g. OwnedStreamWrapper and UnownedStreamWrapper. The former can contain a std::istream directly (which is moveable, but not copyable) and the latter can contain (e.g.) a std::istream&, i.e. have no ownership over the referenced stream instance.

struct OwnedStreamWrapper : public StreamWrapper {
  template <typename... A>
  OwnedStreamWrapper(A &&...a) : stream_{std::forward<A>(a)...} {}
  std::istream &stream() override { return stream_; }

 private:
  std::ifstream stream_;
};

struct UnownedStreamWrapper : public StreamWrapper {
  UnownedStreamWrapper(std::istream &stream) : stream_{stream} {}
  std::istream &stream() override { return stream_; }

 private:
  std::istream &stream_;
};

Apart from the stream ownership, wrapper ownership matters as well and can be used either to handle the wrappers’ (and streams’) lifespan automatically on the stack, or to allow a wrapper’s lifespan to exceed that of the current stack frame. In the example below, the first ReadWrapper() takes ownership of the wrapper and deletes it automatically whereas the second ReadWrapper() does not take ownership. In both cases polymorphism (i.e. the implementation of StreamWrapper) determines how the underlying stream is handled, i.e. whether it outlives its wrapper (like std::cin should) or dies together with it (like a manually instantiated stream should).

void ReadWrite(std::istream &in, std::ostream &out) {
  std::array<char, 1024> buffer;
  while (in) {
    in.read(buffer.data(), buffer.size());
    out.write(buffer.data(), in.gcount());
  }
}

void ReadWrapper(std::unique_ptr<StreamWrapper> stream) {
  ReadWrite(stream->stream(), std::cout);
}

void ReadWrapper(StreamWrapper &stream) {
  ReadWrite(stream.stream(), std::cout);
}

Below are all four combinations of { owned | unowned } { streams | stream wrappers } in a runnable example:

#include <array>
#include <fstream>
#include <iostream>
#include <memory>
#include <utility>

namespace {

/********** The three snippets above go here! **********/

}  // namespace

int main() {
  OwnedStreamWrapper stream1{"/proc/cpuinfo"};
  std::unique_ptr<StreamWrapper> stream2{
      std::make_unique<OwnedStreamWrapper>("/proc/cpuinfo")};
  std::ifstream stream3_file{"/proc/cpuinfo"};  // Let’s pretend it is unowned.
  UnownedStreamWrapper stream3{stream3_file};
  std::ifstream stream4_file{"/proc/cpuinfo"};  // Let’s pretend it is unowned.
  std::unique_ptr<StreamWrapper> stream4{
      std::make_unique<UnownedStreamWrapper>(stream4_file)};

  ReadWrapper(stream1);             // owned stream, wrapper kept
  ReadWrapper(std::move(stream2));  // owned stream, wrapper transferred
  ReadWrapper(stream3);             // unowned stream, wrapper kept
  ReadWrapper(std::move(stream4));  // unowned stream, wrapper transferred
}
Andrej Podzimek
  • 2,409
  • 9
  • 12
1

A smart pointer is kind of an awkward fit for this situation, because it requires a dynamic allocation (which is probably not necessary in this case)


1. Do your processing in another function

As @BoP pointed out in the comments a nice way to handle this could be to use another function that gets passed the appropriate input stream:

godbolt

int do_thing(std::istream& in_stream) {
    // do stuff with the input, regardless of the source...
    int foobar;
    in_stream >> foobar;

    return 0;
}


int main(int argc, char const* argv[]) {
    std::ofstream{"/tmp/foobar"} << 1234;

    if(argc > 1) {
        std::ifstream file{argv[1]};
        if(!file) return -1;

        return do_thing(file);
    } else {
        return do_thing(std::cin);
    }
}

2. Switch the stream buffer

If you won't be using std::cin at all if a filename is passed you could change the streambuffer of std::cin to the one of the file with rdbuf() - that way you can use std::cin in both cases.

godbolt

std::ifstream file;
std::streambuf* oldbuf = std::cin.rdbuf();
    
if (argc > 1) {
    file.open(argv[1]);
    if (!file) return -1;

    // switch buffer of std::cin
    std::cin.rdbuf(file.rdbuf());
}

// do stuff with the input, regardless of the source...
int foobar;
std::cin >> foobar; // this will reader either from std::cin or the file, depending on the current streambuffer assigned to std::cin


// restore old stream buffer
std::cin.rdbuf(oldbuf);

3. Using std::unique_ptr / std::shared_ptr with a custom deleter

The easiest solution if you still want to use smart pointers would be to use std::unique_ptr / std::shared_ptr with a custom deleter that handles the std::cin case:

godbolt

// deletes the stream only if it is not std::cin
struct istream_ptr_deleter {
    void operator()(std::istream* stream) {
        if(&std::cin == stream) return;
        delete stream;
    }
};

using istream_ptr = std::unique_ptr<std::istream, istream_ptr_deleter>;


// Example Usage:
istream_ptr in_stream;
if (argc > 1) {
    in_stream.reset(new std::ifstream(argv[1]));
    if (!*in_stream) return -1;
} else {
    in_stream.reset(&std::cin);
}

int foobar;
*in_stream >> foobar;
Turtlefight
  • 9,420
  • 2
  • 23
  • 40