C++ конвертировать простые значения в строку

Прямо сейчас я использую следующий фрагмент кода для случайного преобразования основных типов (int, long, char[]такого рода вещи), чтобы std::string для дальнейшей обработки:

template<class T>
constexpr std::string stringify(const T& t)
{
    std::stringstream ss;
    ss << t;
    return ss.str();
}

Однако мне не нравится тот факт, что это зависит от std::stringstream, Я пытался с помощью std::to_string (из репертуара С ++11), однако, он задыхается char[] переменные.

Есть ли простой способ предложить элегантное решение этой проблемы?

6 ответов

Насколько я знаю, единственный способ сделать это - специализировать шаблон по типу параметра с помощью SFINAE.

Вы должны включить type_traits.

Поэтому вместо вашего кода используйте что-то вроде этого:

template<class T>
 typename std::enable_if<std::is_fundamental<T>::value, std::string>::type stringify(const T& t)
  {
    return std::to_string(t);
  }

template<class T>
  typename std::enable_if<!std::is_fundamental<T>::value, std::string>::type  stringify(const T& t)
  {
    return std::string(t);
  }

этот тест работает для меня:

int main()
{
  std::cout << stringify(3.0f);
  std::cout << stringify("Asdf");
}

Важное примечание: массивы символов, передаваемые этой функции, должны быть завершены нулем!

Как отмечено в комментариях yakk, вы можете избавиться от нулевого завершения с помощью:

template<size_t N> std::string stringify( char(const& s)[N] ) { 
    if (N && !s[N-1]) return {s, s+N-1};
    else return {s, s+N}; 
}

Есть ли простой способ предложить элегантное решение этой проблемы?

Так как никто не предложил это, рассмотрите использование boost:: lexical_cast.

Это легко интегрируется со всем, что реализует оператор std::ostream<< и может быть расширено для пользовательских типов.

Я бы порекомендовал использовать enable_if_t и если вы собираетесь принимать какие-либо символьные переменные, вы специализируете их:

template<typename T>
enable_if_t<is_arithmetic<T>::value, string> stringify(T t){
    return to_string(t);
}

template<typename T>
enable_if_t<!is_arithmetic<T>::value, string> stringify(T t){
    return static_cast<ostringstream&>(ostringstream() << t).str();
}

template<>
string stringify<char>(char t){
    return string(1, t);
}

Здесь я просто специализируюсь char, Если вам нужно специализироваться wchar, char16, или же char32 Вы должны будете сделать это также.

В любом случае для неарифметических типов эти перегрузки будут по умолчанию использовать ostringstream что является хорошей причиной, если вы перегрузили оператор извлечения для одного из ваших классов, это сработает.

Для арифметических типов это будет использовать to_string, за исключением char и все остальное, что вы перегружаете, и те могут непосредственно создать string,

Редактировать:

Дип предложил использовать ли to_string принимает аргумент T::type Как мой enable_if_t состояние.

Самое простое решение доступно вам, только если у вас есть доступ к is_detected в #include <experimental/type_traits>, Если вы просто определите:

template<typename T>
using to_string_t = decltype(to_string(declval<T>()));

Тогда вы можете настроить свой код как:

template<typename T>
decltype(to_string(T{})) stringify(T t){
    return to_string(t);
}

template<typename T>
enable_if_t<!experimental::is_detected<to_string_t, T>::value, string> (T t){
    return static_cast<ostringstream&>(ostringstream() << t).str();
}

template<>
string stringify<char>(char t){
    return string(1, t);
}

Я задал этот вопрос, чтобы выяснить, как использовать to_string как мое состояние Если у вас нет доступа к is_detected Я очень рекомендую прочитать некоторые ответы, потому что они феноменальны: Метапрограммирование: ошибка определения функции определяет отдельную функцию

Я считаю, что самое элегантное решение - это:

#include <string>

template <typename T>
typename std::enable_if<std::is_constructible<std::string, T>::value, std::string>::type
stringify(T&& value) {
    return std::string(std::forward<T>(value)); // take advantage of perfect forwarding
}

template <typename T>
typename std::enable_if<!std::is_constructible<std::string, T>::value, std::string>::type
stringify(T&& value) {
    using std::to_string; // take advantage of ADL (argument-dependent lookup)
    return to_string(std::forward<T>(value)); // take advantage of perfect forwarding
}

Здесь, если мы можем построить std::string с помощью T (мы проверяем это с помощью std::is_constructible<std::string, T>), тогда мы делаем это, в противном случае мы используем to_string,

Конечно, в C++14 вы можете заменить typename std::enable_if<...>::type с гораздо короче std::enable_if_t<...>, Пример приведен в более короткой версии кода, ниже.

Ниже приведена более короткая версия, но она менее эффективна, потому что для этого требуется один дополнительный ход std::string (но если вместо этого мы сделаем только копию, это будет еще менее эффективно):

#include <string>

std::string stringify(std::string s) { // use implicit conversion to std::string
    return std::move(s); // take advantage of move semantics
}

template <typename T>
std::enable_if_t<!std::is_convertible<T, std::string>::value, std::string>
stringify(T&& value) {
    using std::to_string; // take advantage of ADL (argument-dependent lookup)
    return to_string(std::forward<T>(value)); // take advantage of perfect forwarding
}

Эта версия использует неявное преобразование в std::string тогда возможно, и использует to_string иначе. Обратите внимание на использование std::move чтобы использовать в C++11 семантику перемещения.

Вот почему мое решение лучше, чем наиболее популярное в настоящее время решение @cerkiewny:

  • Он имеет гораздо более широкое применение, потому что, благодаря ADL, он также определен для любого типа, для преобразования которого используется функция to_stringопределяется (не только std:: версия этого), см. пример использования ниже. Принимая во внимание, что решение @cerkiewny работает только для фундаментальных типов и для типов, из которых строится std::string.

    Конечно, в его случае можно добавить дополнительные перегрузкиstringify для других типов, но это гораздо менее надежное решение по сравнению с добавлением новых версий ADL to_string, И шансы на высоту, что ADL-совместимый to_string уже определен в сторонней библиотеке для типа, который мы хотим использовать. В этом случае с моим кодом вам вообще не нужно писать никакого дополнительного кода, чтобы stringify Работа.

  • Это более эффективно, потому что использует преимущества совершенной пересылки C++11 (с использованием универсальных ссылок (T&&) а также std::forward).

Пример использования:

#include <string>

namespace Geom {
    class Point {
    public:
        Point(int x, int y) : x(x), y(y) {}

        // This function is ADL-compatible and not only 'stringify' can benefit from it.
        friend std::string to_string(const Point& p) {
            return '(' + std::to_string(p.x) + ", " + std::to_string(p.y) + ')';
        }
    private:
        int x;
        int y;
    };
}

#include <iostream>
#include "stringify.h" // inclusion of the code located at the top of this answer

int main() {
    double d = 1.2;
    std::cout << stringify(d) << std::endl; // outputs "1.200000"

    char s[] = "Hello, World!";
    std::cout << stringify(s) << std::endl; // outputs "Hello, World!"

    Geom::Point p(1, 2);
    std::cout << stringify(p) << std::endl; // outputs "(1, 2)"
}

Альтернативный, но не рекомендуемый подход

Я тоже считал просто перегрузку to_string:

template <typename T>
typename std::enable_if<std::is_constructible<std::string, T>::value, std::string>::type
to_string(T&& value) {
    return std::string(std::forward<T>(value)); // take advantage of perfect forwarding
}

И более короткая версия, использующая неявное преобразование в std::string:

std::string to_string(std::string s) { // use implicit conversion to std::string
    return std::move(s); // take advantage of move semantics
}

Но у них есть серьезные ограничения: нам нужно помнить, чтобы написать to_string вместо std::to_string везде, где мы хотим его использовать; также он несовместим с наиболее распространенным шаблоном использования ADL:

int main() {
    std::string a = std::to_string("Hello World!"); // error

    using std::to_string; // ADL
    std::string b = to_string("Hello World!"); // error
}

И, скорее всего, есть другие проблемы, связанные с этим подходом.

Самое простое решение - перегрузка для типов, которые вы хотите:

using std::to_string;

template<size_t Size>
std::string to_string(const char (&arr)[Size])
{
    return std::string(arr, Size - 1);
}

поскольку to_string это не шаблон, вы не можете специализировать его, но, к счастью, это проще.

Код предполагает, что массив завершен нулем, но все еще безопасен, если это не так.

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

Это также имеет то преимущество, что если вы как-то передаете ей строку, не заканчивающуюся нулем, в качестве одного аргумента не будет UB std::string конструктор делает.

Хотя вопрос не имеет ничего общего с кодом, поскольку у меня уже есть решение, я решил поделиться им:

template <class... Tail>
inline auto buildString(std::string const &head, Tail const &... tail)
    -> std::string;

template <class... Tail>
inline auto buildString(char const *head, Tail const &... tail) -> std::string;

template <class... Tail>
inline auto buildString(char *head, Tail const &... tail) -> std::string;

template <class Head, class... Tail>
inline auto buildString(Head const &head, Tail const &... tail) -> std::string;

inline auto buildString() -> std::string { return {}; }

template <class... Tail>
inline auto buildString(std::string const &head, Tail const &... tail)
    -> std::string {
  return head + buildString(tail...);
}
template <class... Tail>
inline auto buildString(char const *head, Tail const &... tail) -> std::string {
  return std::string{head} + buildString(tail...);
}
template <class... Tail>
inline auto buildString(char *head, Tail const &... tail) -> std::string {
  return std::string{head} + buildString(tail...);
}
template <class Head, class... Tail>
inline auto buildString(Head const &head, Tail const &... tail) -> std::string {
  return std::to_string(head) + buildString(tail...);
}

Использование:

auto gimmeTheString(std::string const &str) -> void {
  cout << str << endl;
}

int main() {

  std::string cpp_string{"This c++ string"};
  char const c_string[] = "this c string";

  gimmeTheString(buildString("I have some strings: ", cpp_string, " and ",
                             c_string, " and some number ", 24));
  return 0;
}
Другие вопросы по тегам