Заранее извиняюсь за плохое название, не уверен, как назвать то, что я пытаюсь сделать.
Некоторый фон, если вы не хотите читать, перейдите к следующему абзацу. У меня есть класс модульного теста, где я вызываю assert с некоторым условием, и, если он не выполняется, я вывожу некоторую строку, которая была передана. Я обнаружил, что довольно неудобно создавать строку для отправки в нее, если, например, я хочу сказать "Failed on index: " + i
, Моя идея состоит в том, чтобы вернуть std::ostream
вместо того, чтобы взять std::string
, Если утверждение не удается, я возвращаюсь std::cerr
, если утверждение проходит, то я возвращаю std::stringstream
, Я думаю, я мог бы сделать все это просто отлично. Я должен был бы хранить std::stringstream
в моем классе модульного теста, чтобы я мог вернуть ссылку.
То, что я хотел бы сделать, это вместо того, чтобы вернуть стандарт std::ostream
вернуть расширенный std::ostream
что выводит std::endl
когда это сделано, мне не нужно запоминать это для каждого утверждения. В частности, идея заключается в следующем:
UnitTest("My test");
ut.assert(false) << "Hello world";
ut.assert(1 == 0) << "On the next line";
Идея заключается в том, что при уничтожении этот новый класс будет выводить конечную линию и будет уничтожен, как только он больше не будет использоваться (т.е. больше не будет). << операторы). Пока это то, что у меня есть (я удалил часть кода в assert, и он на самом деле внутри класса, но этого достаточно, чтобы показать, что происходит):
class newline_ostream : public std::ostream
{
public:
newline_ostream(std::ostream& other) : std::ostream(other.rdbuf()){}
~newline_ostream() { (*this) << std::endl; }
};
newline_ostream& assert(bool condition, std::string error)
{
if(!condition)
{
return newline_ostream(std::cerr);
}
return newline_ostream(std::stringstream());
}
Когда я пробую этот метод, я получаю некоторые вещи, в основном говорящие мне, что возвращать объект, который я только что создал, неправильно, потому что это не lvalue. Когда я пытаюсь изменить его так, чтобы он не возвращал ссылку, он жалуется, что нет конструктора копирования (вероятно, это потому, что я расширяю std::ostream
и у него нет конструктора копирования).
То, что я ищу, это какой-то метод, который заставляет компилятор создать временный newline_ostream, который assert()
запишет свой результат в тот, который умрет, как только он больше не будет использоваться (т.е. больше не будет << операторы). Возможно ли это и если да, то как?
копирование std::cerr
невозможно (конструктор копирования std::basic_ostream
удаляется). Поэтому создание производного класса, реализующего конструктор копирования, на самом деле не вариант.
Я бы предложил вам создать свой newline_ostream
как класс, который содержит ссылку на (а не является производным от) std::ostream
:
#include <iostream>
class newline_ostream
{
std::ostream &_strm;
public:
explicit newline_ostream(std::ostream &strm)
:_strm(strm)
{}
/* In the destructor, we submit a final 'endl'
before we die, as desired. */
virtual ~newline_ostream() {
_strm << std::endl;
}
template <typename T>
newline_ostream &operator<<(const T& t) {
_strm << t;
return *this;
}
};
int main()
{
newline_ostream s(std::cerr);
s << "This is a number " << 3 << '\n';
/* Here we make a copy (using the default copy
constructor of the new class), just to show
that it works. */
newline_ostream s2(s);
s2 << "This is another number: " << 12;
return 0;
}
Может быть ересью, но вместо того, чтобы получить поток для создания другого типа потока, более обобщенным способом может быть определение манипулятора:
// compile with g++ -std=c++11 -Wall -pedantic
#include <iostream>
class sassert
{
public:
sassert(bool b) :ps(), good(b)
{}
friend std::ostream& operator<<(std::ostream& s, sassert&& a)
{ a.ps = &s; if(a.good) s.setstate(s.failbit); return s; }
~sassert()
{
if(good && ps) ps->clear();
if(!good && ps) *ps << std::endl;
}
//move semantics allow sassert to be a result of a calculation
sassert(sassert&& s) :ps(s.ps), good(s.good) { s.ps=nullptr; }
sassert& operator=(sassert s){ ps=s.ps; good=s.good; s.ps=0; return *this; }
private:
std::ostream* ps;
bool good;
};
int main()
{
std::cout << sassert(false) << "this is a failed assertion";
std::cout << sassert(true) << "this is a good assertion";
std::cout << sassert(false) << "this is another failed assertion";
std::cout << sassert(true) << "this is another good assertion";
return 0;
}
Побежит продюсировать
this is a failed assertion
this is another failed assertion
Это действительно зависит от специфики того, чего вы хотите достичь.
Если вы никогда не слышали о Тип Туннелирование, например, это может быть хороший момент, чтобы прочитать об этом. Есть способ использовать прокладки, чтобы делать сумасшедшие вещи …
В противном случае, вот простая версия:
class AssertMessage {
public:
AssertMessage(): _out(nullptr) {}
AssertMessage(std::ostream& out): _out(&out) {}
AssertMessage(AssertMessage&& other): _out(other._out) { other._out = nullptr; }
AssertMessage& operator=(AssertMessage&& other) {
if (_out) { _out << "\n"; }
_out = other._out;
other._out = nullptr;
return *this;
}
~AssertMessage() { if (_out) { _out << "\n"; } }
template <typename T>
AssertMessage& operator<<(T const& t) {
if (_out) { *_out << t; }
}
private:
std::ostream* _out;
}; // class AssertMessage
Заметьте, как при встраивании указателя нам не нужен глобальный «нулевой» объект? Это основное различие между указателями и ссылками. Также обратите внимание на использование конструктора перемещения / оператора присваивания перемещения, чтобы избежать вывода 2 новые строки или больше.
Затем вы можете написать assert
метод:
AssertMessage UnitTest::assert(bool i) {
return i ? AssertMessage() : AssertMessage(std::cerr);
}
Однако …. Я бы серьезно подумал об использовании макроса на вашем месте, потому что вы получаете дополнительные привилегии:
#define UT_ASSERT(Cond_) \
assert(Cond_, #Cond_, __func__, __FILE__, __LINE__)
AssertMessage assert(bool test,
char const* condition,
char const* func,
char const* file,
int line)
{
if (test) { return AssertMessage(); }
return AssertMessage(std::cerr <<
"Failed assert at " << file << "#" << line <<
" in " << func << ": '" << condition << "', ");
}
И тогда вы получите что-то вроде:
Failed assert at project/test.cpp#45 in foo: 'x != 85', <your message>
В больших тестовых пакетах неоценимо иметь имя файла и номер строки (как минимум).
Наконец, макрос дает вам еще больше: если вы вызываете функцию в вашем сообщении, такую как ut.assert(x) << x.foo();
, затем x.foo()
необходимо оценить полностью, даже если сообщение не будет напечатано; это довольно расточительно. Однако с макросом:
#define UT_ASSERT(Cond_, Message_) \
while (!(Cond_)) { std::cerr << .... << Message_ << '\n'; break; }
тогда, если условие оценивается как true
тело while
не выполняется вообще.
Вы также можете использовать препроцессор для этого:
#define U_ASSERT(ut, cond, stream) \
do { ut.assert(cond) << stream << std::endl; } while (0)
U_ASSERT(ut, 1 == 0, "The result is " << (1 == 0));
Тем не менее, этот метод и метод, который вы уже используете (с изменениями от jogojapan), являются в основном единственными альтернативами. Это связано с тем, что даже если вы начинаете работать с буферами, вы не можете сказать, когда выполняется одна операция вывода, а затем начинается следующая, поэтому вы не знаете, когда вставить новую строку.
Я нашел способ, который работает для меня (в частности, я возвращаю копию, а не ссылку):
class newline_ostream : public std::ostream
{
public:
newline_ostream(const std::ostream& other) : std::ostream(other.rdbuf()){}
newline_ostream(const newline_ostream& other) : std::ostream(other.rdbuf()){}
~newline_ostream() { (*this) << std::endl; }
};
newline_ostream assert(bool condition, std::string error)
{
if(!condition)
{
return newline_ostream(std::cerr);
}
return newline_ostream(nullStream); // nullStream defined elsewhere (in the class)
}
Использование:
ut.assert(ps.getFaces()[i] == expected[i], ss.str()) << "Test " << i;
Выход:
Test 0
Test 1
Test 2
Test 3
Test 5
Test 7
Test 9
Test 10