Як імплодувати вектор рядків у рядок (елегантний спосіб)


83

Я шукаю найбільш елегантний спосіб імплодіювати вектор рядків у рядок. Нижче наведено рішення, яке я використовую зараз:

static std::string& implode(const std::vector<std::string>& elems, char delim, std::string& s)
{
    for (std::vector<std::string>::const_iterator ii = elems.begin(); ii != elems.end(); ++ii)
    {
        s += (*ii);
        if ( ii + 1 != elems.end() ) {
            s += delim;
        }
    }

    return s;
}

static std::string implode(const std::vector<std::string>& elems, char delim)
{
    std::string s;
    return implode(elems, delim, s);
}

Є ще хтось там?


Чому ви називаєте цю функцію імплодією?
Colonel Panic

5
@ColonelPanic, за аналогією з методом implode () PHP, який об'єднує елементи масиву і виводить їх у вигляді одного рядка. Цікаво, чому ви задаєте це питання :)
ezpresso

Відповіді:


133

Використання boost::algorithm::join(..):

#include <boost/algorithm/string/join.hpp>
...
std::string joinedString = boost::algorithm::join(elems, delim);

Дивіться також це питання .


57
Запропонувати включити та створити посилання проти масивної бібліотеки підвищення для створення простого рядка абсурдно.
Джуліан

8
@Julian більшість проектів це вже роблять. Я згоден з тим, що абсурдно, що STL вже не містить способу зробити це. Я також можу погодитися, що це не повинна бути основною відповіддю, але інші відповіді чітко доступні.
Рівер Там

Я погоджуюсь з @Julian. Підсилення може бути елегантним у використанні, але це ніяк не "найелегантніший спосіб" з точки зору накладних витрат. У цьому випадку це обхід алгоритму OP, а не вирішення самого питання.
Azeroth2b

3
Більшість бібліотек Boost мають лише заголовки, тому немає нічого для зв’язку. Деякі навіть проникають у стандарт.
jbruni

28
std::vector<std::string> strings;

const char* const delim = ", ";

std::ostringstream imploded;
std::copy(strings.begin(), strings.end(),
           std::ostream_iterator<std::string>(imploded, delim));

( В тому числі <string>, <vector>, <sstream>і <iterator>)

Якщо ви хочете мати чистий кінець (без кінцевого роздільника), подивіться тут


9
майте на увазі, однак, що це додасть додатковий роздільник (другий параметр до std::ostream_iteratorконструктора в кінці потоку.
Майкл Крелін - хакер

9
Суть "розпаду" полягає в тому, що роздільник не слід додавати останнім. Ця відповідь, на жаль, додає, що роздільник останній.
Джонні,

І на щастя мені також потрібно додати маркер останнім! Дякую за рішення.
Константин Ван

20

Вам слід використовувати std::ostringstreamзамість того, std::stringщоб будувати вихідні дані (тоді ви можете викликати його str()метод в кінці, щоб отримати рядок, тому ваш інтерфейс не повинен змінюватися, лише тимчасовий s).

З цього моменту ви можете перейти до використання std::ostream_iterator, наприклад:

copy(elems.begin(), elems.end(), ostream_iterator<string>(s, delim)); 

Але це має дві проблеми:

  1. delimтепер повинен бути const char*, а не єдиним char. Нічого страшного.
  2. std::ostream_iteratorпише роздільник після кожного окремого елемента, включаючи останній. Отже, вам потрібно буде стерти останню в кінці, або написати власну версію ітератора, яка не має цього роздратування. Варто зробити останнє, якщо у вас багато коду, який потребує подібних речей; інакше найкраще уникнути всього безладу (тобто використовувати, ostringstreamале ні ostream_iterator).

1
Або скористайтеся вже написаним: stackoverflow.com/questions/3496982/…
Джеррі Коффін

13

Оскільки я люблю однокласні вкладиші (вони дуже корисні для всяких дивних речей, як ви побачите в кінці), ось рішення, яке використовує std :: accumulate та лямбда-програму C ++ 11:

std::accumulate(alist.begin(), alist.end(), std::string(), 
    [](const std::string& a, const std::string& b) -> std::string { 
        return a + (a.length() > 0 ? "," : "") + b; 
    } )

Я вважаю цей синтаксис корисним для оператора потоку, де я не хочу, щоб всі види дивної логіки виходили за рамки операції потоку, а лише для простого об'єднання рядків. Розглянемо, наприклад, цей оператор return із методу, який форматує рядок за допомогою операторів потоку (за допомогою std;):

return (dynamic_cast<ostringstream&>(ostringstream()
    << "List content: " << endl
    << std::accumulate(alist.begin(), alist.end(), std::string(), 
        [](const std::string& a, const std::string& b) -> std::string { 
            return a + (a.length() > 0 ? "," : "") + b; 
        } ) << endl
    << "Maybe some more stuff" << endl
    )).str();

Оновлення:

Як зазначав @plexando в коментарях, наведений вище код страждає від неправильної поведінки, коли масив починається з порожніх рядків через те, що в перевірці на "перший запуск" відсутні попередні прогони, які не призвели до додаткових символів, а також - дивно запускати перевірку на "перший запуск" на всіх прогонах (тобто код недостатньо оптимізований).

Рішення обох цих проблем є простим, якщо ми точно знаємо, що список містить принаймні один елемент. ОТО, якщо ми точно знаємо, що список не має хоча б одного елемента, тоді ми можемо ще більше скоротити цикл.

Я думаю, що отриманий код не такий гарний, тому я додаю його сюди як Правильне рішення , але я думаю, що обговорення вище все-таки має merrit:

alist.empty() ? "" : /* leave early if there are no items in the list
  std::accumulate( /* otherwise, accumulate */
    ++alist.begin(), alist.end(), /* the range 2nd to after-last */
    *alist.begin(), /* and start accumulating with the first item */
    [](auto& a, auto& b) { return a + "," + b; });

Примітки:

  • Для контейнерів, що підтримують прямий доступ до першого елемента, можливо, краще використовувати це для третього аргументу, а не alist[0]для векторів.
  • Відповідно до обговорення в коментарях та чаті, лямбда все ще виконує копіювання. Це можна звести до мінімуму, використовуючи замість цього (менш красиву) лямбду: [](auto&& a, auto&& b) -> auto& { a += ','; a += b; return a; })яка (на GCC 10) покращує продуктивність більш ніж на x10. Дякую @Deduplicator за пропозицію. Я все ще намагаюся зрозуміти, що тут відбувається.

4
Не використовуйте accumulateдля рядків. Більшість інших відповідей - O (n), але accumulateO (n ^ 2), оскільки він створює тимчасову копію накопичувача перед додаванням кожного елемента. І ні, семантика переміщення не допомагає.
Oktalist

2
@Oktalist, я не впевнений, чому ти це кажеш - cplusplus.com/reference/numeric/accumulate говорить "Складність лінійна на відстані між першим і останнім".
Guss

1
Це припускаючи, що кожне окреме додавання займає постійний час. Якщо у Tвас є перевантаження operator+(як stringце робиться) або якщо ви надаєте власний функтор, тоді всі ставки відключені. Хоча я, можливо, поспішав із твердженням, що семантика переміщення не допомагає, вони не вирішують проблему в двох реалізаціях, які я перевірив. Перегляньте мої відповіді на подібні запитання .
Oktalist

1
Коментар skwllsp тут нічого спільного. Як я вже сказав, більшість інших відповідей (і implodeприклад ОП ) роблять правильно. Вони мають значення O (n), навіть якщо вони не викликають reserveрядок. Тільки розчин, що використовує накопичувач, - O (n ^ 2). Не потрібно код в стилі С.
Окталіст,

12
Я зробив орієнтир , і накопичення насправді було швидшим, ніж потік рядків O (n).
kirbyfan64sos

11

а як щодо простого дурного рішення?

std::string String::join(const std::vector<std::string> &lst, const std::string &delim)
{
    std::string ret;
    for(const auto &s : lst) {
        if(!ret.empty())
            ret += delim;
        ret += s;
    }
    return ret;
}

8
string join(const vector<string>& vec, const char* delim)
{
    stringstream res;
    copy(vec.begin(), vec.end(), ostream_iterator<string>(res, delim));
    return res.str();
}

7

Мені подобається використовувати цей однорядковий накопичувач (без кінцевого роздільника):

std::accumulate(
    std::next(elems.begin()), 
    elems.end(), 
    elems[0], 
    [](std::string a, std::string b) {
        return a + delimiter + b;
    }
);

4
Будьте обережні, коли порожньо.
Карлос Пінзон,

6

Особливо для більших колекцій, вам потрібно уникнути необхідності перевіряти, додаєте ви все-таки перший елемент чи ні, щоб переконатися, що немає роздільника ...

Отже, для порожнього або одноелементного списку ітерація взагалі відсутня.

Порожні діапазони тривіальні: повернення "".

З одним елементом або багатоелементом можна ідеально впоратися accumulate:

auto join = [](const auto &&range, const auto separator) {
    if (range.empty()) return std::string();

    return std::accumulate(
         next(begin(range)), // there is at least 1 element, so OK.
         end(range),

         range[0], // the initial value

         [&separator](auto result, const auto &value) {
             return result + separator + value;
         });
};

Запуск зразка ( потрібен C ++ 14 ): http://cpp.sh/8uspd


3

Версія, яка використовує std::accumulate:

#include <numeric>
#include <iostream>
#include <string>

struct infix {
  std::string sep;
  infix(const std::string& sep) : sep(sep) {}
  std::string operator()(const std::string& lhs, const std::string& rhs) {
    std::string rz(lhs);
    if(!lhs.empty() && !rhs.empty())
      rz += sep;
    rz += rhs;
    return rz;
  }
};

int main() {
  std::string a[] = { "Hello", "World", "is", "a", "program" };
  std::string sum = std::accumulate(a, a+5, std::string(), infix(", "));
  std::cout << sum << "\n";
}

2

Ось ще один, який не додає роздільник після останнього елемента:

std::string concat_strings(const std::vector<std::string> &elements,
                           const std::string &separator)
{       
    if (!elements.empty())
    {
        std::stringstream ss;
        auto it = elements.cbegin();
        while (true)
        {
            ss << *it++;
            if (it != elements.cend())
                ss << separator;
            else
                return ss.str();
        }       
    }
    return "";

2

Використовуючи частину цієї відповіді на інше запитання, ви отримуєте об’єднане питання на основі роздільника без кінцевої коми

Використання:

std::vector<std::string> input_str = std::vector<std::string>({"a", "b", "c"});
std::string result = string_join(input_str, ",");
printf("%s", result.c_str());
/// a,b,c

Код:

std::string string_join(const std::vector<std::string>& elements, const char* const separator)
{
    switch (elements.size())
    {
        case 0:
            return "";
        case 1:
            return elements[0];
        default:
            std::ostringstream os;
            std::copy(elements.begin(), elements.end() - 1, std::ostream_iterator<std::string>(os, separator));
            os << *elements.rbegin();
            return os.str();
    }
}

1

Ось що я використовую, просте та гнучке

string joinList(vector<string> arr, string delimiter)
{
    if (arr.empty()) return "";

    string str;
    for (auto i : arr)
        str += i + delimiter;
    str = str.substr(0, str.size() - delimiter.size());
    return str;
}

за допомогою:

string a = joinList({ "a", "bbb", "c" }, "!@#");

вихід:

a!@#bbb!@#c

0

Трохи довге рішення, але не використовує std::ostringstreamі не вимагає злому для видалення останнього роздільника.

http://www.ideone.com/hW1M9

І код:

struct appender
{
  appender(char d, std::string& sd, int ic) : delim(d), dest(sd), count(ic)
  {
    dest.reserve(2048);
  }

  void operator()(std::string const& copy)
  {
    dest.append(copy);
    if (--count)
      dest.append(1, delim);
  }

  char delim;
  mutable std::string& dest;
  mutable int count;
};

void implode(const std::vector<std::string>& elems, char delim, std::string& s)
{
  std::for_each(elems.begin(), elems.end(), appender(delim, s, elems.size()));
}

0

Можливе рішення за допомогою потрійного оператора ?:.

std::string join(const std::vector<std::string> & v, const std::string & delimiter = ", ") {
    std::string result;

    for (size_t i = 0; i < v.size(); ++i) {
        result += (i ? delimiter : "") + v[i]; 
    }

    return result;
}

join({"2", "4", "5"})дасть вам 2, 4, 5.


0

З fmt ви можете це зробити.

#include <fmt/format.h>
auto s = fmt::format("{}",fmt::join(elems,delim)); 

Але я не знаю, чи приєднається до нього std :: format.



-1

спробуйте це, але використовуючи вектор замість списку

template <class T>
std::string listToString(std::list<T> l){
    std::stringstream ss;
    for(std::list<int>::iterator it = l.begin(); it!=l.end(); ++it){
        ss << *it;
        if(std::distance(it,l.end())>1)
            ss << ", ";
    }
    return "[" + ss.str()+ "]";
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.