Как я могу сделать ссылку ostream на ofstream? (С++)

Я пытаюсь создать простой класс регистратора, и мне нужна возможность вести журнал либо в общий ostream (cout/cerr), либо в файл. Дизайн, который я имею в виду, заключается в том, чтобы позволить конструктору либо взять ostream&, либо имя файла, а в последнем случае создать ofstream& и назначить его частному ostream& класса, например так:

class Log {
private:
    std::ostream& os;
public:
    Log(std::ostream& os = std::cout): os(os) { }
    Log(std::string filename) {
        std::ofstream ofs(filename);
        if (!ofs.is_open())
            // do errorry things
        os = ofs;
    }
};

Это дает мне ошибку, что оператор присваивания ofstream является закрытым. Просматривая это снова, я понял, что создание ссылки на локальный объект, вероятно, не сработает, а создание os указателя на ostream, а также объявление и удаление его в куче работало в случае ofstream, хотя и не в случае ostream. случай, когда ostream уже существует и на него просто ссылается os (поскольку единственное место для удаления os будет в конструкторе, и я не знаю, как определить, указывает ли os на ofstream, созданный на куча или нет).

Итак, как я могу заставить это работать, то есть сделать os ссылкой на ofstream, инициализированную именем файла в конструкторе?


person Community    schedule 27.02.2012    source источник


Ответы (4)


Во-первых, вы не можете повторно привязать ссылки после их создания, вы можете только инициализировать их. Вы можете подумать, что можете сделать это:

Log(std::string filename) : os(std::ofstream(filename)) {
    if (!os.is_open())
        // do errorry things
}

Но это нехорошо, потому что вы заставляете os ссылаться на временную переменную.

Когда вам нужна ссылка, которая должна быть необязательна, то есть иногда она должна ссылаться на что-то, а не на что-то другое, вам действительно нужен указатель:

class Log {
private:
    std::ostream* os;
    bool dynamic;
public:
    Log(std::ostream& os = std::cout): os(&os), dynamic(false) { }
    Log(std::string filename) : dynamic(true) {
        std::ofstream* ofs = new std::ofstream(filename);

        if (!ofs->is_open())
            // do errorry things and deallocate ofs if necessary

        os = ofs;
    }

    ~Log() { if (dynamic) delete os; }
};

Приведенный выше пример просто показывает вам, что происходит, но вы, вероятно, захотите управлять им с помощью интеллектуального указателя. Как указывает Бен Фойгт, существует множество подводных камней, которые могут привести к непредвиденному и нежелательному поведению вашей программы; например, когда вы пытаетесь сделать копию вышеприведенного класса, это приведет к провалу. Вот пример вышеизложенного с использованием интеллектуальных указателей:

class Log {
private:
    std::unique_ptr<std::ostream, std::function<void(std::ostream*)>> os;
public:
    Log(std::ostream& os = std::cout): os(&os, [](ostream*){}) { }

    Log(std::string filename) : os(new std::ofstream(filename), std::default_delete<std::ostream>()) {
        if (!dynamic_cast<std::ofstream&>(*os).is_open())
            // do errorry things and don't have to deallocate os
    }
};

Необычный os(&os, [](ostream*){}) заставляет указатель указывать на данный ostream&, но ничего не делает, когда он выходит за пределы области видимости; он дает ему функцию удаления, которая ничего не делает. Вы можете сделать это и без лямбда-выражений, просто для этого примера это проще.

person Seth Carnegie    schedule 27.02.2012
comment
Итак, предполагая, что я все еще создаю поток в конструкторе, действительно нет никакого способа избежать объявления/удаления os в куче, не так ли? - person ; 27.02.2012
comment
@Anachrome нет, другого пути нет, потому что вы хотите создать его в одном конструкторе (а не в другом), но оставить его последним после выхода из конструктора. Единственный способ сделать это, о котором я знаю, - это выделить его динамически. - person Seth Carnegie; 27.02.2012
comment
Ой! Осторожнее с деструктором! { Log l; } попытается позвонить delete &std::cout;! Там нужен флаг. - person R. Martinho Fernandes; 27.02.2012
comment
Колодец. Я буду управлять. А что касается вашего кода, не возникнет ли проблем с удалением в деструкторе, если он был создан с помощью первого конструктора (и, следовательно, не был динамически выделен)? Конечно, я думаю, это всего лишь еще одна причина использовать умные указатели. - person ; 27.02.2012
comment
@Anachrome да, это точно. Это также то, о чем упомянул Р. Мартиньо Фернандеш, и я исправил это. - person Seth Carnegie; 27.02.2012
comment
Не лучше ли иметь внутренний объект ofstream и объект ostream*, указывающий либо на внутренний ofstream, либо на внешний ostream, чтобы избежать динамического распределения и удалить этот уродливый флаг? - person André Caron; 27.02.2012
comment
@AndréCaron это то, что сделал ответ Наваза, но по какой-то причине он его удалил. Я забыл, что ofstream может быть построен по умолчанию, когда писал этот ответ. - person Seth Carnegie; 27.02.2012
comment
Вы должны упомянуть, зачем нужен умный указатель. Код примера — это полная катастрофа, ожидающая своего часа, так как компилятор считает, что его можно преобразовать, и сгенерирует конструктор копирования, который делает это. И это только одна из многих потенциальных ошибок - person Ben Voigt; 27.02.2012
comment
К сожалению, тип удаления является явным параметром шаблона unique_ptr. Это означает, что вы не можете использовать его для хранения детерминаторов разных типов. Вам нужно какое-то стирание типа, либо с std::function<void(void*)>, либо с shared_ptr. - person R. Martinho Fernandes; 27.02.2012
comment
Еще лучше может быть простой функтор удаления, который также несет флаг и действует соответствующим образом. Здесь нет необходимости в сложном стирании типов и лямбда-выражениях. - person Xeo; 27.02.2012
comment
+1, если вы можете реализовать конструктор копирования! Серьезно, мне нужно уметь делать Log log(cerr); Log log2(log); log2.output("hey"); - person Marc Eaddy; 15.11.2013

Самое простое, что нужно сделать, это просто привязать свою ссылку к ofstream и убедиться, что ofstream живет так же долго, как ваш объект:

class Log
{
    std::ofstream byname;
    std::ostream& os;
public:
    Log(std::ostream& stream = std::cout) : byname(), os(stream) { }
    Log(std::string filename) : byname(filename), os(this->byname)
    {
        if (!os)
            // handle errors
    }
};

Безопасны для исключений, не могут протекать, а специальные функции-члены, сгенерированные компилятором, в порядке.

person Ben Voigt    schedule 27.02.2012
comment
В простоте этого есть определенная красота, которая на самом деле заставляет меня думать, что я мог бы в конечном итоге использовать это вместо ответа выше. Хотя еще не совсем решил. - person ; 28.02.2012

В моем классе Log/Debug я считаю полезным создать статическую переменную-член:

class debug {
  public:
    ...

    // Overload operator() for printing values.
    template<class Type1>
      inline debug&
      operator()(const std::string& name1,
                 const Type1& value1)
      {
        // Prettify the name/value someway in another inline function.
        _stream << print_value(name1, value1) << std::endl;

        return *this;
      }

  private:
    ...
    static std::ostream& _stream;
};

А затем в моем файле debug.cc:

std::ostream& debug::_stream = std::cerr;
person nerozehl    schedule 27.02.2012

Вы должны инициализировать os в списке инициализации в конструкторах, точно так же, как вы делали это в Log(std::ostream& os = std::cout): os_(os) { }, потому что os является ссылкой, которая не может быть назначена после инициализации.

person Ade YU    schedule 27.02.2012
comment
Только если он сделает os ссылкой на константу. - person jrok; 27.02.2012
comment
@jrok, и тогда он не может писать или читать с него. И даже тогда ofstream будет уничтожен в конце конструктора. - person Seth Carnegie; 27.02.2012
comment
@Сет Верно. РЕДАКТИРОВАТЬ: нет, вторая часть неверна. Ссылка на константу (и временную с ней) будет действительна в течение всего времени существования объекта класса. - person jrok; 27.02.2012
comment
@jrok: Нет, только локальные переменные могут продлить срок службы временных. Переменные-члены этого не делают. - person R. Martinho Fernandes; 27.02.2012
comment
@Мартиньо. Ну, я честно этого не знал. :) Спасибо. - person jrok; 27.02.2012
comment
@jrok на самом деле, если ссылка const, то временная будет жить до конца конструктора - person Seth Carnegie; 27.02.2012