Pull to refresh

Реализация ToString() на С++

Reading time 9 min
Views 19K
Для вывода в лог (да и не только для этого, но это то, с чем я сам столкнулся) нужно конвертировать значение переменной в строку.

В 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
Tags:
Hubs:
+27
Comments 39
Comments Comments 39

Articles