Для вывода в лог (да и не только для этого, но это то, с чем я сам столкнулся) нужно конвертировать значение переменной в строку.
В C++ это обычно делается выводом в поток (как вариант — использование boost: lexical_cast<> — что в нашем случае практически одно и тоже).
Для встроенных типов это не проблема, а вот как быть, если нужно вывести скажем std: vector? Увы, но у std: vector нет оператора вывода в поток.
В результате решения этой проблемы написал код, которым хочу поделиться с сообществом.
Итак, сначала основная идея. Идея, собственно, весьма проста — написать набор перегруженных функций, которые будут выполнять преобразование в строку.
Первый вопрос, который перед нами встает — это какой прототип функции использовать:
или
Второй вариант кажется более привлекательным — передаем переменную — возвращает строку.
Но с точки зрения производительности (нам ведь нужна производительность — иначе зачем мы полезли в C++) первый вариант обычно выигрывает, так как не создается временная переменная (типа std: wstring) для возвращаемого значения.
К тому-же простая обертка без проблем дает нам и второй вариант:
Первая проблема решена, теперь переходим собственно к реализации ToStream (). Самый простой вариант это вывод через оператор вывода (простите за тавтологию).
Стоп! А что если у типа нет оператора вывода? Наткнулись на первоначальную проблему. Решение очевидно — нужно разрешить эту функцию только для типов, для которых оператор вывода в поток определен. В коде это выглядит так:
Отлично, первый этап пройден.
Что дальше? Определим вывод для типа std: pair — будем выводить в виде "(T, U)“:
Кто-то задал вопрос? Повторите пожалуйста — на расстоянии 10 000 километров плохо слышно…
Зачем мы [рекурсивно] вызываем ToStream ()? Все очень просто. Дело в том, что типы T и/или U в свою очередь могут быть сложными типами, например std: pair<int, std::pair<int, int> >. В случае рекурсивного вызова получим вывод в виде (0, (1, 2)), что нам собственно и надо.
Наступил звездный час и для стандартных контейнеров (выводим в виде "[3](1, 2, 3)»):
Теперь определим преобразование для типа bool. К счастью это проще, чем предыдущие функции. Только одно маленькое замечание — в моем коде в хидере (.h) только описание функции, а определение вынесено в .cpp файл. Причина проста — если хидер включается в несколько .cpp файлов, то функция определяется в нескольких единицах трансляции, что есть плохо и линковшик нам об этом сообщит (злорадствуя по поводу своего превосходства). Для шаблонных функций этого не происходит. Исключительно для простоты я перенес определение функции в хидер (что не следует делать для рабочих проектов по причине, описаной выше).
Вот, в кратце, и все основные функции. Правда в моей реализации есть еще:
Для чего? Для вывода в «широкий» поток (std: wostream) «узких» строк/символов (char, std: string). Дело в том, что в своих проектах я имею дело со строками в формате UTF8. Соответственно храню я такие строки в std: string. В функциях ToStream (std: wostream& strm, const std: string& val) я преобразую строку из UFT8 в std: wstring и вывожу ее. Код функций не привожу, так как его усложнит, а ничего принципиального нового не принесет.
Теперь примеры использования.
Для начала пара вспомогательных макросов (не надо кидать в меня камнями! Иногда макросы могут сильно облегчить жизнь).
Первый макрос:
позволяет нам написать код:
и получить в выводе:
почему не «i=0 n=10»? Причина ощущается при выводе строк:
вывод (если подсветка кода не даст сбой, то разница будет очевидна):
Второй макрос для тестов — если условие не выполняется, то кидает исключение:
Теперь собственно примеры:
Наиболее простой из примеров.
Этом пример немного интересней тем, что в нем используется 2 функции — для контейнера и для std: pair (напоминаю, что map хранит в себе пары) — вот для чего мы писали вывод пар.
Надеюсь, что никого не удивляет, что вывод отличается от того, что написано в инициализации. Если кого-то это все-таки удивляет, то советую вспомнить что такое std: map и как там хранятся данные.
Сначала код.
Собстенно ничего интересного — enum приводится к целому типу и выводится его значение. Я бы не стал приводить этот банальный пример, если бы не возможность расширения моего решения. Добавляем следующий код (опять макрос! да, я в курсе, но мне так проще):
и о чудо! Вывод превращается в:
Т.е. в этом примере показано как расширить возможности применения моего решения.
PS: В реальной реализации все обернуто в пространства имен. Полные листинги (с более приятной подсветкой синтаксиса) здесь:
RO4_ToString.h
main.cpp
В C++ это обычно делается выводом в поток (как вариант — использование boost: lexical_cast<> — что в нашем случае практически одно и тоже).
Для встроенных типов это не проблема, а вот как быть, если нужно вывести скажем std: vector? Увы, но у std: vector нет оператора вывода в поток.
В результате решения этой проблемы написал код, которым хочу поделиться с сообществом.
Основная идея.
Итак, сначала основная идея. Идея, собственно, весьма проста — написать набор перегруженных функций, которые будут выполнять преобразование в строку.
Первый вопрос, который перед нами встает — это какой прототип функции использовать:
template<typename T>
void ToStream(std::wostream& strm, const T& val);
или
template<typename T>
std::wstring ToString(const T& val);
Второй вариант кажется более привлекательным — передаем переменную — возвращает строку.
Но с точки зрения производительности (нам ведь нужна производительность — иначе зачем мы полезли в C++) первый вариант обычно выигрывает, так как не создается временная переменная (типа std: wstring) для возвращаемого значения.
К тому-же простая обертка без проблем дает нам и второй вариант:
template<typename T>
std::wstring ToString(const T& val)
{
std::wostringstream strm;
ToStream(strm, val);
return strm.str();
}
Первая проблема решена, теперь переходим собственно к реализации ToStream (). Самый простой вариант это вывод через оператор вывода (простите за тавтологию).
template <typename T>
void ToStream(std::wostream& strm, const T& val)
{
strm << val;
}
Стоп! А что если у типа нет оператора вывода? Наткнулись на первоначальную проблему. Решение очевидно — нужно разрешить эту функцию только для типов, для которых оператор вывода в поток определен. В коде это выглядит так:
// Любой тип T может быть неявно приведен к данному типу
// Используется в нижеследующей функции
struct AnyType
{
template <class T>
AnyType(T)
{
}
};
// Оператор вывода
// Используется для детектирования типов, которые не имееют оператора вывода (operator<<)
template <class Char>
boost::type_traits::no_type operator<<(std::basic_ostream<Char>&, AnyType);
// Можно ли вывести тип T в поток (есть ли у типа T operator<<)?
template <class T, class Char>
class IsOutStreamable
{
static std::basic_ostream<Char>& GetStrm();
static const T& GetT();
static boost::type_traits::no_type Impl(boost::type_traits::no_type);
static boost::type_traits::yes_type Impl(...);
public:
static const bool value = sizeof(Impl(GetStrm() << GetT())) == sizeof(boost::type_traits::yes_type);
};
// === Используя оператор вывода для типа T
template <typename T>
typename boost::enable_if_c<IsOutStreamable<T, wchar_t>::value, void>::type
ToStream(std::wostream& strm, const T& val)
{
strm << val;
}
Отлично, первый этап пройден.
Что дальше? Определим вывод для типа std: pair — будем выводить в виде "(T, U)“:
// === std::pair
template<typename T, typename U>
void ToStream(std::wostream& strm, const std::pair<T, U>& val)
{
strm << L'(';
ToStream(strm, val.first);
strm << L", ";
ToStream(strm, val.second);
strm << L')';
}
Кто-то задал вопрос? Повторите пожалуйста — на расстоянии 10 000 километров плохо слышно…
Зачем мы [рекурсивно] вызываем ToStream ()? Все очень просто. Дело в том, что типы T и/или U в свою очередь могут быть сложными типами, например std: pair<int, std::pair<int, int> >. В случае рекурсивного вызова получим вывод в виде (0, (1, 2)), что нам собственно и надо.
Наступил звездный час и для стандартных контейнеров (выводим в виде "[3](1, 2, 3)»):
// Определяем has_iterator и т.д.
BOOST_MPL_HAS_XXX_TRAIT_DEF(iterator);
BOOST_MPL_HAS_XXX_TRAIT_DEF(const_iterator);
BOOST_MPL_HAS_XXX_TRAIT_DEF(value_type);
// Структура для теста "является ли тип стандартным контейнером (STL container)"
// Считаем, что тип это контейнер, если он содержит определение типов
// для iterator, const_iterator и value_type, но не является std::[w]string
template<typename T>
struct IsStdContainer
{
static const int value = boost::mpl::and_<
has_iterator<T>,
has_const_iterator<T>,
has_value_type<T>,
boost::mpl::not_<boost::is_same<T, std::string> >,
boost::mpl::not_<boost::is_same<T, std::wstring> >
>::value;
};
// === STL контейнеры (и то, что выглядит как STL контейнеры - см. IsStdContainer выше)
template<typename T>
typename boost::enable_if<IsStdContainer<T>, void>::type
ToStream(std::wostream& strm, const T& val)
{
strm << L'[' << val.size() << L"](";
if ( !val.empty() )
{
typename T::const_iterator it = val.begin();
ToStream(strm, *it++);
for (; it != val.end(); ++it)
{
strm << L", ";
ToStream(strm, *it);
}
}
strm << L')';
}
Теперь определим преобразование для типа bool. К счастью это проще, чем предыдущие функции. Только одно маленькое замечание — в моем коде в хидере (.h) только описание функции, а определение вынесено в .cpp файл. Причина проста — если хидер включается в несколько .cpp файлов, то функция определяется в нескольких единицах трансляции, что есть плохо и линковшик нам об этом сообщит (злорадствуя по поводу своего превосходства). Для шаблонных функций этого не происходит. Исключительно для простоты я перенес определение функции в хидер (что не следует делать для рабочих проектов по причине, описаной выше).
// === bool
void ToStream(std::wostream& strm, const bool& val)
{
strm << ( val ? L"true" : L"false" );
}
Вот, в кратце, и все основные функции. Правда в моей реализации есть еще:
// === std::string
void ToStream(std::wostream& strm, const std::string& val);
// === char*
void ToStream(std::wostream& strm, char* val);
// === const char*
void ToStream(std::wostream& strm, const char* val);
// === const char
void ToStream(std::wostream& strm, const char val);
Для чего? Для вывода в «широкий» поток (std: wostream) «узких» строк/символов (char, std: string). Дело в том, что в своих проектах я имею дело со строками в формате UTF8. Соответственно храню я такие строки в std: string. В функциях ToStream (std: wostream& strm, const std: string& val) я преобразую строку из UFT8 в std: wstring и вывожу ее. Код функций не привожу, так как его усложнит, а ничего принципиального нового не принесет.
Теперь примеры использования.
Для начала пара вспомогательных макросов (не надо кидать в меня камнями! Иногда макросы могут сильно облегчить жизнь).
Первый макрос:
#define _VAR(var) L ## #var << L"<" << ToString(var) << L"> "
позволяет нам написать код:
int i = 0;
int n = 10;
std::cout << _VAR(i) << _VAR(n);
и получить в выводе:
i<0> n<10>
почему не «i=0 n=10»? Причина ощущается при выводе строк:
std::string s1 = "";
std::string s2 = " ";
std::cout << _VAR(s1) << _VAR(s2);
вывод (если подсветка кода не даст сбой, то разница будет очевидна):
s1<> s2< >
Второй макрос для тестов — если условие не выполняется, то кидает исключение:
#define CHECK(expr) \
if ( !( expr ) ) \
{ \
throw #expr; \
} \
else \
((void)0)
Теперь собственно примеры:
Пример 1. Вывод std: vector.
Наиболее простой из примеров.
std::vector<int> v = boost::assign::list_of(0)(1)(2)(3);
CHECK(ToString(v) == L"[4](0, 1, 2, 3)");
std::wcout << _VAR(v) << std::endl;
Пример 2. Вывод std: map.
Этом пример немного интересней тем, что в нем используется 2 функции — для контейнера и для std: pair (напоминаю, что map хранит в себе пары) — вот для чего мы писали вывод пар.
std::map<int, int> m = boost::assign::map_list_of(0, 1)(2, 3)(4, 5);
CHECK(ToString(m) == L"[3]((0, 1), (2, 3), (4, 5))");
std::wcout << _VAR(m) << std::endl;
<h4>Пример 3. Снова вывод std::map.</h4>
Этом пример еще интересней. В в качестве значений используется векторы.
<code class="cpp">
std::map<std::wstring, std::vector<int> > msv = boost::assign::list_of< std::pair<std::wstring, std::vector<int> > >
( L"zero", boost::assign::list_of(0) )
( L"one", boost::assign::list_of(1)(2) )
( L"two", boost::assign::list_of(2)(3)(4) )
;
CHECK(ToString(msv) == L"[3]((one, [2](1, 2)), (two, [3](2, 3, 4)), (zero, [1](0)))");
std::wcout << _VAR(msv) << std::endl;
Надеюсь, что никого не удивляет, что вывод отличается от того, что написано в инициализации. Если кого-то это все-таки удивляет, то советую вспомнить что такое std: map и как там хранятся данные.
Пример 4. Вывод пользовательских типов.
Сначала код.
enum RO4_ReplyType /// Reply type
{
RO4_RT_Mobile, ///< Replies go to mobile phone
RO4_RT_Email, ///< Replies go to email address
RO4_RT_MobileAndEmail ///< Replies go to mobile phone and to email address
};
RO4_ReplyType rt = RO4_RT_Email;
CHECK(RO4::Manip::ToString(rt) == L"1");
Собстенно ничего интересного — enum приводится к целому типу и выводится его значение. Я бы не стал приводить этот банальный пример, если бы не возможность расширения моего решения. Добавляем следующий код (опять макрос! да, я в курсе, но мне так проще):
/// Output operator for RO4_ReplyType
void ToStream(std::wostream& strm, const RO4_ReplyType& val)
{
#define STR(name) case name: strm << L## #name; break
switch ( val )
{
STR(RO4_RT_Mobile);
STR(RO4_RT_Email);
STR(RO4_RT_MobileAndEmail);
default:
strm << L"Unknown value of RO4_ReplyType<" << static_cast<int>(val) << L">";
}
#undef STR
}
и о чудо! Вывод превращается в:
RO4_ReplyType rt = RO4_RT_Email;
CHECK(RO4::Manip::ToString(rt) == L"RO4_RT_Email");
Т.е. в этом примере показано как расширить возможности применения моего решения.
Послесловие.
PS: В реальной реализации все обернуто в пространства имен. Полные листинги (с более приятной подсветкой синтаксиса) здесь:
RO4_ToString.h
main.cpp