Jak implodować wektor strun w strunę (elegancki sposób)

83

Szukam najbardziej eleganckiego sposobu na implodowanie wektora strun w ciąg. Poniżej znajduje się rozwiązanie, którego teraz używam:

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);
}

Czy są tam inni?

ezpresso
źródło
Dlaczego nazywasz tę funkcję implodowaniem?
Colonel Panic
5
@ColonelPanic, analogicznie do metody implode () PHP, która łączy elementy tablicy i wyświetla je jako pojedynczy ciąg. Zastanawiam się, dlaczego
zadajesz

Odpowiedzi:

133

Zastosowanie boost::algorithm::join(..):

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

Zobacz także to pytanie .

Andre Holzner
źródło
57
Sugerowanie dołączania i łączenia z ogromną biblioteką boost w celu utworzenia prostego ciągu jest absurdalne.
Julian
8
@Julian większość projektów już to robi. Zgadzam się, że to absurd, że STL nie zawiera już sposobu, aby to zrobić. Mogę również zgodzić się, że nie powinna to być najlepsza odpowiedź, ale inne odpowiedzi są wyraźnie dostępne.
River Tam
Zgadzam się z @Julian. Boost może być elegancki w użyciu, ale nie jest „najbardziej eleganckim sposobem” pod względem kosztów ogólnych. W tym przypadku jest to obejście algorytmu PO, a nie rozwiązanie samego pytania.
Azeroth2b
3
Większość bibliotek Boost obsługuje tylko nagłówki, więc nie ma nic do łączenia. Niektórzy nawet wchodzą do standardu.
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));

(obejmuje <string>, <vector>, <sstream>i <iterator>)

Jeśli chcesz mieć czysty koniec (bez końcowego separatora), zajrzyj tutaj

sehe
źródło
9
pamiętaj jednak, że doda to dodatkowy separator (drugi parametr do std::ostream_iteratorkonstruktora na końcu strumienia.
Michael Krelin - haker
9
Celem „implodowania” jest to, że ogranicznik nie powinien być dodawany na końcu. Ta odpowiedź niestety dodaje, że separator jest ostatni.
Jonny
I na szczęście token muszę dodać również na końcu! Dzięki za rozwiązanie.
Константин Ван
20

Powinieneś std::ostringstreamraczej używać niż std::stringbudować dane wyjściowe (wtedy możesz wywołać jego str()metodę na końcu, aby uzyskać ciąg, więc twój interfejs nie musi się zmieniać, tylko tymczasowy s).

Stamtąd możesz zmienić na używanie std::ostream_iterator, na przykład:

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

Ale ma to dwa problemy:

  1. delimteraz musi być const char*, a nie pojedynczym char. Nie ma sprawy.
  2. std::ostream_iteratorzapisuje separator po każdym elemencie, łącznie z ostatnim. Więc musiałbyś albo usunąć ostatni na końcu, albo napisać własną wersję iteratora, która nie ma tej irytacji. Warto zrobić to drugie, jeśli masz dużo kodu, który potrzebuje takich rzeczy; w przeciwnym razie najlepiej byłoby uniknąć całego bałaganu (tj. używać, ostringstreamale nie ostream_iterator).
John Zwinck
źródło
1
Lub użyj już napisanego: stackoverflow.com/questions/3496982/…
Jerry Coffin
13

Ponieważ kocham jednolinijki (są bardzo przydatne do wszelkiego rodzaju dziwnych rzeczy, jak zobaczysz na końcu), oto rozwiązanie wykorzystujące std ::umulate i C ++ 11 lambda:

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

Uważam, że ta składnia jest przydatna w przypadku operatora strumienia, w którym nie chcę mieć wszelkiego rodzaju dziwnej logiki poza zakresem operacji strumienia, tylko po to, aby wykonać proste łączenie ciągu. Rozważmy na przykład tę instrukcję return from method, która formatuje ciąg przy użyciu operatorów strumienia (using 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();

Aktualizacja:

Jak zauważył @plexando w komentarzach, powyższy kod cierpi na niewłaściwe zachowanie, gdy tablica zaczyna się od pustych ciągów, ponieważ w sprawdzaniu „pierwszego uruchomienia” brakuje poprzednich uruchomień, które nie spowodowały żadnych dodatkowych znaków, a także - dziwne jest uruchamianie sprawdzania „jest uruchamiane jako pierwsze” we wszystkich przebiegach (tj. kod jest niedoptymalizowany).

Rozwiązanie obu tych problemów jest łatwe, jeśli wiemy na pewno, że lista zawiera co najmniej jeden element. OTOH, jeśli wiemy na pewno, że lista nie zawiera przynajmniej jednego elementu, to możemy skrócić przebieg jeszcze bardziej.

Myślę, że wynikowy kod nie jest tak ładny, więc dodaję go tutaj jako The Correct Solution , ale myślę, że powyższa dyskusja nadal ma 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; });

Uwagi:

  • W przypadku kontenerów, które obsługują bezpośredni dostęp do pierwszego elementu, prawdopodobnie lepiej jest użyć tego dla trzeciego argumentu, a więc alist[0]dla wektorów.
  • Zgodnie z dyskusją w komentarzach i czacie, lambda nadal kopiuje. Można to zminimalizować, używając zamiast tego tej (mniej ładnej) lambdy: [](auto&& a, auto&& b) -> auto& { a += ','; a += b; return a; })która (w GCC 10) poprawia wydajność o więcej niż x10. Dzięki @Deduplicator za sugestię. Nadal próbuję dowiedzieć się, co się tutaj dzieje.
Guss
źródło
4
Nie używaj accumulatedo ciągów. Większość pozostałych odpowiedzi to O (n), ale accumulatejest to O (n ^ 2), ponieważ tworzy tymczasową kopię akumulatora przed dołączeniem każdego elementu. I nie, semantyka ruchu nie pomaga.
Oktalist
2
@Oktalist, nie jestem pewien, dlaczego tak mówisz - cplusplus.com/reference/numeric/accumulate mówi „Złożoność jest liniowa w odniesieniu do odległości między pierwszym a ostatnim”.
Guss
1
To przy założeniu, że każdy indywidualny dodatek zajmuje stały czas. Jeśli Tma przeciążony operator+(tak jak stringrobi) lub jeśli zapewniasz własny funktor, wszystkie zakłady są wyłączone. Chociaż być może spieszyłem się, mówiąc, że semantyka ruchu nie pomaga, nie rozwiązują problemu w dwóch sprawdzonych implementacjach. Zobacz moje odpowiedzi na podobne pytania .
Oktalist
1
Komentarz skwllsp nie ma z tym nic wspólnego. Jak powiedziałem, większość pozostałych odpowiedzi (i implodeprzykład PO ) jest słuszna. Są O (n), nawet jeśli nie wywołują reservełańcucha. Tylko rozwiązanie używające akumuluj to O (n ^ 2). Nie ma potrzeby stosowania kodu w stylu C.
Oktalist
12
Wykonałem test porównawczy i akumulacja była w rzeczywistości szybsza niż strumień O (n).
kirbyfan64sos
11

co z prostym głupim rozwiązaniem?

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;
}
user3545770
źródło
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();
}
Czarna Mamba
źródło
7

Lubię używać tej jednowierszowej akumulacji (bez końcowego ogranicznika):

std::accumulate(
    std::next(elems.begin()), 
    elems.end(), 
    elems[0], 
    [](std::string a, std::string b) {
        return a + delimiter + b;
    }
);
Sasa Milenkovic
źródło
4
Uważaj, gdy jest pusty.
Carlos Pinzón,
6

Szczególnie w przypadku większych kolekcji chcesz uniknąć konieczności sprawdzania, czy nadal dodajesz pierwszy element, czy nie, aby upewnić się, że nie ma końcowego separatora ...

Tak więc w przypadku listy pustej lub jednoelementowej nie ma żadnej iteracji.

Puste zakresy są trywialne: zwraca „”.

Jednoelementowy lub wieloelementowy doskonale radzi sobie z 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;
         });
};

Uruchomiony przykład ( wymaga C ++ 14 ): http://cpp.sh/8uspd

xtofl
źródło
3

Wersja wykorzystująca 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";
}
Robᵩ
źródło
2

Oto kolejny, który nie dodaje separatora po ostatnim elemencie:

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 "";
Darien Pardinas
źródło
2

Użycie części tej odpowiedzi na inne pytanie daje połączenie this, oparte na separatorze bez końcowego przecinka,

Stosowanie:

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

Kod:

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();
    }
}
TankorSmash
źródło
1

Oto, czego używam, proste i elastyczne

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;
}

za pomocą:

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

wynik:

a!@#bbb!@#c
user1438233
źródło
0

Nieco długie rozwiązanie, ale nie używa std::ostringstream i nie wymaga hakowania w celu usunięcia ostatniego ogranicznika.

http://www.ideone.com/hW1M9

A kod:

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()));
}
Nim
źródło
0

Możliwe rozwiązanie z operatorem trójskładnikowym ?:.

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"})da ci 2, 4, 5.

Ynjxsjmh
źródło
0

Z FMT możesz to zrobić.

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

Ale nie wiem, czy join przejdzie do std :: format.

andreas777
źródło
-1

poprostu dodaj !! String s = "";

for (int i = 0; i < doc.size(); i++)   //doc is the vector
    s += doc[i];
czysta gra
źródło
-1

spróbuj tego, ale użyj wektora zamiast listy

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()+ "]";
}
D. Pérez
źródło