Wie implodiere ich einen Vektor von Strings in einen String (auf elegante Weise)

83

Ich suche nach der elegantesten Möglichkeit, einen String-Vektor in einen String zu implodieren. Unten ist die Lösung, die ich jetzt verwende:

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

Gibt es da draußen noch andere?

Ezpresso
quelle
Warum nennt man diese Funktion implodieren?
Colonel Panic
5
@ColonelPanic, analog zur implode () -Methode von PHP, die Array-Elemente verbindet und als einzelne Zeichenfolge ausgibt. Ich frage mich, warum Sie diese Frage stellen :)
Ezpresso

Antworten:

132

Verwendung boost::algorithm::join(..):

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

Siehe auch diese Frage .

Andre Holzner
quelle
55
Es ist absurd vorzuschlagen, die massive Boost-Bibliothek einzuschließen und mit ihr zu verknüpfen, um eine einfache Zeichenfolge zu erstellen.
Julian
8
@ Julian Die meisten Projekte machen das schon. Ich stimme zu, dass es absurd ist, dass die STL jedoch noch keine Möglichkeit enthält, dies zu tun. Ich könnte auch zustimmen, dass dies nicht die beste Antwort sein sollte, aber andere Antworten sind eindeutig verfügbar.
Fluss Tam
Ich stimme @Julian zu. Boost ist zwar elegant im Gebrauch, aber in Bezug auf den Overhead keineswegs der "eleganteste Weg". In diesem Fall ist dies eine Problemumgehung für den OP-Algorithmus und keine Lösung für die Frage selbst.
Azeroth2b
3
Die meisten Boost-Bibliotheken sind nur Header, daher gibt es nichts zu verknüpfen. Einige schaffen es sogar in den Standard.
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));

(gehört <string>, <vector>, <sstream>und <iterator>)

Wenn Sie ein sauberes Ende haben möchten (kein abschließendes Trennzeichen), schauen Sie hier

sehe sehen
quelle
9
Beachten Sie jedoch, dass ein zusätzlicher Begrenzer hinzugefügt wird (der zweite Parameter für den std::ostream_iteratorKonstruktor am Ende des Streams.
Michael Krelin - Hacker
9
Der Punkt von "implodieren" ist, dass ein Trennzeichen nicht zuletzt hinzugefügt werden sollte. Diese Antwort fügt leider das letzte Trennzeichen hinzu.
Jonny
20

Sie sollten die Ausgabe nicht verwenden, std::ostringstreamsondern std::stringerstellen (dann können Sie die str()Methode am Ende aufrufen , um eine Zeichenfolge abzurufen, sodass sich Ihre Schnittstelle nicht ändern muss, sondern nur die temporäre s).

Von dort aus können Sie wie folgt zur Verwendung wechseln std::ostream_iterator:

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

Dies hat jedoch zwei Probleme:

  1. delimJetzt muss es const char*eher eine als eine einzige sein char. Keine große Sache.
  2. std::ostream_iteratorschreibt das Trennzeichen nach jedem einzelnen Element, einschließlich des letzten. Sie müssten also entweder den letzten am Ende löschen oder eine eigene Version des Iterators schreiben, die diesen Ärger nicht hat. Letzteres lohnt sich, wenn Sie viel Code haben, der solche Dinge benötigt. Andernfalls könnte das ganze Durcheinander am besten vermieden werden (dh verwenden, ostringstreamaber nicht ostream_iterator).
John Zwinck
quelle
1
Oder verwenden Sie eine, die bereits geschrieben wurde: stackoverflow.com/questions/3496982/…
Jerry Coffin
13

Da ich Einzeiler liebe (sie sind sehr nützlich für alle Arten von seltsamen Dingen, wie Sie am Ende sehen werden), gibt es hier eine Lösung mit std :: accumulate und 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; 
    } )

Ich finde diese Syntax nützlich für Stream-Operatoren, bei denen ich nicht alle Arten von seltsamer Logik außerhalb des Bereichs der Stream-Operation haben möchte, nur um einen einfachen String-Join durchzuführen. Betrachten Sie zum Beispiel diese return-Anweisung von einer Methode, die eine Zeichenfolge mithilfe von Stream-Operatoren (mithilfe von std;) formatiert:

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

Aktualisieren:

Wie von @plexando in den Kommentaren hervorgehoben, weist der obige Code ein Fehlverhalten auf, wenn das Array mit leeren Zeichenfolgen beginnt, da bei der Prüfung auf "erster Lauf" frühere Läufe fehlen, die zu keinen zusätzlichen Zeichen geführt haben, und außerdem - Es ist seltsam, bei allen Läufen nach "wird zuerst ausgeführt" zu suchen (dh der Code ist unteroptimiert).

Die Lösung für diese beiden Probleme ist einfach, wenn wir wissen, dass die Liste mindestens ein Element enthält. OTOH, wenn wir sicher wissen, dass die Liste nicht mindestens ein Element enthält, können wir den Lauf noch weiter verkürzen.

Ich denke, der resultierende Code ist nicht so hübsch, also füge ich ihn hier als The Correct Solution hinzu , aber ich denke, die obige Diskussion hat immer noch 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; });

Anmerkungen:

  • Für Container, die den direkten Zugriff auf das erste Element unterstützen, ist es wahrscheinlich besser, dies stattdessen für das dritte Argument zu verwenden, also alist[0]für Vektoren.
  • Gemäß der Diskussion in den Kommentaren und im Chat kopiert das Lambda immer noch. Dies kann minimiert werden, indem stattdessen dieses (weniger hübsche) Lambda verwendet wird, [](auto&& a, auto&& b) -> auto& { a += ','; a += b; return a; })das (bei GCC 10) die Leistung um mehr als x10 verbessert. Vielen Dank an @Deduplicator für den Vorschlag. Ich versuche immer noch herauszufinden, was hier los ist.
Guss
quelle
4
Nicht accumulatefür Strings verwenden. Die meisten anderen Antworten sind O (n), aber accumulateO (n ^ 2), da vor dem Anhängen jedes Elements eine temporäre Kopie des Akkumulators erstellt wird. Und nein, Bewegungssemantik hilft nicht.
Oktalist
2
@Oktalist, ich bin mir nicht sicher, warum Sie das sagen - cplusplus.com/reference/numeric/accumulate sagt "Komplexität ist linear im Abstand zwischen erstem und letztem".
Guss
1
Dies setzt voraus, dass jede einzelne Addition eine konstante Zeit benötigt. Wenn Teine Überlastung vorliegt operator+(wie dies der stringFall ist) oder wenn Sie Ihren eigenen Funktor bereitstellen, sind alle Wetten ungültig. Obwohl ich vielleicht hastig gesagt habe, dass die Bewegungssemantik nicht hilft, lösen sie das Problem in den beiden von mir überprüften Implementierungen nicht. Siehe meine Antworten auf ähnliche Fragen .
Oktalist
1
Der Kommentar von skwllsp hat nichts damit zu tun. Wie gesagt, die meisten anderen Antworten (und das implodeBeispiel des OP ) machen das Richtige. Sie sind O (n), auch wenn sie reservedie Zeichenfolge nicht aufrufen . Nur die Lösung mit akkumulieren ist O (n ^ 2). Kein C-Code erforderlich.
Oktalist
12
Ich habe einen Benchmark durchgeführt und die Akkumulation war tatsächlich schneller als ein O (n) -String-Stream.
kirbyfan64sos
10

Was ist mit einer einfachen dummen Lösung?

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
quelle
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();
}
Schwarze Mamba
quelle
7

Ich verwende gerne diesen Einzeiler (kein nachfolgendes Trennzeichen):

std::accumulate(
    std::next(elems.begin()), 
    elems.end(), 
    elems[0], 
    [](std::string a, std::string b) {
        return a + delimiter + b;
    }
);
Sasa Milenkovic
quelle
4
Sei vorsichtig, wenn es leer ist.
Carlos Pinzón
6

Insbesondere bei größeren Sammlungen möchten Sie vermeiden, dass Sie überprüfen müssen, ob Sie noch das erste Element hinzufügen oder nicht, um sicherzustellen, dass kein nachfolgendes Trennzeichen vorhanden ist.

Für die leere Liste oder die Liste mit einzelnen Elementen gibt es also überhaupt keine Iteration.

Leere Bereiche sind trivial: return "".

Einzel- oder Mehrfachelemente können perfekt gehandhabt werden durch 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;
         });
};

Ausführendes Beispiel ( C ++ 14 erforderlich ): http://cpp.sh/8uspd

xtofl
quelle
3

Eine Version, die verwendet 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ᵩ
quelle
2

Hier ist eine andere, die das Trennzeichen nach dem letzten Element nicht hinzufügt:

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
quelle
2

Wenn Sie einen Teil dieser Antwort auf eine andere Frage verwenden, erhalten Sie eine Verknüpfung, basierend auf einem Trennzeichen ohne nachfolgendes Komma.

Verwendung:

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

Code:

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
quelle
1

Folgendes verwende ich, einfach und flexibel

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

mit:

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

Ausgabe:

a!@#bbb!@#c
user1438233
quelle
0

Etwas lange Lösung, wird aber nicht verwendet std::ostringstreamund erfordert keinen Hack, um das letzte Trennzeichen zu entfernen.

http://www.ideone.com/hW1M9

Und der Code:

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
quelle
0

Eine mögliche Lösung mit ternärem Operator ?:.

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"})werde dir geben 2, 4, 5.

Ynjxsjmh
quelle
-1

einfach hinzufügen !! String s = "";

for (int i = 0; i < doc.size(); i++)   //doc is the vector
    s += doc[i];
Cleanplay
quelle
-1

Versuchen Sie dies, aber verwenden Sie den Vektor anstelle der Liste

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
quelle