Cómo implosionar un vector de cuerdas en una cuerda (la forma elegante)

81

Estoy buscando la forma más elegante de convertir un vector de cuerdas en una cuerda. A continuación se muestra la solución que estoy usando ahora:

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

¿Hay otros ahí fuera?

ezpresso
fuente
¿Por qué llamas a esta función implosionar?
Colonel Panic
5
@ColonelPanic, por analogía con el método implode () de PHP, que une elementos de la matriz y los genera como una sola cadena. Me pregunto por qué haces esta pregunta :)
ezpresso

Respuestas:

132

Utilizar boost::algorithm::join(..):

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

Vea también esta pregunta .

Andre Holzner
fuente
54
Sugerir incluir y vincular contra la biblioteca de impulso masivo para crear una cadena simple es absurdo.
Julian
8
@Julian la mayoría de los proyectos ya hacen esto. Sin embargo, estoy de acuerdo en que es absurdo que el STL no incluya una forma de hacer esto. También podría estar de acuerdo en que esta no debería ser la respuesta principal , pero hay otras respuestas claramente disponibles.
River Tam
Estoy de acuerdo con @Julian. Boost puede ser elegante en el uso, pero no es la "forma más elegante" en términos de gastos generales. En este caso, es una solución al algoritmo del OP y no una resolución a la pregunta en sí.
Azeroth2b
3
La mayoría de las bibliotecas de Boost son solo de encabezado, por lo que no hay nada que vincular. Algunos incluso llegan al estándar.
jbruni
27
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));

(incluya <string>, <vector>, <sstream>y <iterator>)

Si desea tener un final limpio (sin delimitador final), eche un vistazo aquí

sehe
fuente
9
Tenga en cuenta, sin embargo, que agregará un delimitador adicional (el segundo parámetro al std::ostream_iteratorconstructor al final de la transmisión.
Michael Krelin - hacker
9
El punto de "implosión" es que no se debe agregar un delimitador al final. Desafortunadamente, esta respuesta agrega ese delimitador al final.
Jonny
20

Debería usar en std::ostringstreamlugar de std::stringgenerar la salida (luego puede llamar a su str()método al final para obtener una cadena, por lo que su interfaz no necesita cambiar, solo la temporal s).

A partir de ahí, podría cambiar a usar std::ostream_iterator, así:

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

Pero esto tiene dos problemas:

  1. delimahora debe ser un const char*, en lugar de uno solo char. No es gran cosa.
  2. std::ostream_iteratorescribe el delimitador después de cada elemento, incluido el último. Por lo tanto, necesitaría borrar el último al final o escribir su propia versión del iterador que no tiene esta molestia. Valdría la pena hacer lo último si tienes mucho código que necesita cosas como esta; de lo contrario, sería mejor evitar todo el lío (es decir, usar ostringstreampero no ostream_iterator).
John Zwinck
fuente
1
O use uno que ya esté escrito: stackoverflow.com/questions/3496982/…
Jerry Coffin
13

Como me encantan las frases ingeniosas (son muy útiles para todo tipo de cosas raras, como verá al final), aquí hay una solución usando std :: acumular y 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; 
    } )

Encuentro esta sintaxis útil con el operador de flujo, donde no quiero tener todo tipo de lógica extraña fuera del alcance de la operación de flujo, solo para hacer una combinación de cadena simple. Considere, por ejemplo, esta declaración de retorno del método que formatea una cadena usando operadores de flujo (usando 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();

Actualizar:

Como lo señaló @plexando en los comentarios, el código anterior sufre un mal comportamiento cuando la matriz comienza con cadenas vacías debido al hecho de que en la verificación de la "primera ejecución" faltan las ejecuciones anteriores que no han dado como resultado caracteres adicionales, y también - es extraño ejecutar una comprobación de "es la primera ejecución" en todas las ejecuciones (es decir, el código no está optimizado).

La solución para ambos problemas es fácil si sabemos con certeza que la lista tiene al menos un elemento. OTOH, si sabemos con certeza que la lista no tiene al menos un elemento, podemos acortar aún más la ejecución.

Creo que el código resultante no es tan bonito, así que lo agrego aquí como La solución correcta , pero creo que la discusión anterior todavía tiene 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; });

Notas:

  • Para los contenedores que admiten acceso directo al primer elemento, probablemente sea mejor usarlo para el tercer argumento, es decir, alist[0]para los vectores.
  • Según la discusión en los comentarios y el chat, el lambda todavía hace algunas copias. Esto se puede minimizar usando esta lambda (menos bonita) en su lugar: [](auto&& a, auto&& b) -> auto& { a += ','; a += b; return a; })que (en GCC 10) mejora el rendimiento en más de x10. Gracias a @Deduplicator por la sugerencia. Todavía estoy tratando de averiguar qué está pasando aquí.
Guss
fuente
4
No lo use accumulatepara cuerdas. La mayoría de las otras respuestas son O (n) pero accumulatees O (n ^ 2) porque hace una copia temporal del acumulador antes de agregar cada elemento. Y no, la semántica de movimientos no ayuda.
Oktalist
2
@Oktalist, no estoy seguro de por qué dice eso: cplusplus.com/reference/numeric/accumulate dice "La complejidad es lineal en la distancia entre el primero y el último".
Guss
1
Eso es asumiendo que cada adición individual toma un tiempo constante. Si Ttiene un sobrecargado operator+(como lo stringhace) o si proporciona su propio functor, todas las apuestas están canceladas. Aunque me apresuré a decir que la semántica de movimientos no ayuda, no resuelven el problema en las dos implementaciones que he comprobado. Vea mis respuestas a preguntas similares .
Oktalist
1
El comentario de skwllsp no tiene nada que ver con eso. Como dije, la mayoría de las otras respuestas (y el implodeejemplo del OP ) están haciendo lo correcto. Son O (n), incluso si no llamanreserve a la cuerda. Solo la solución que usa acumular es O (n ^ 2). No es necesario un código de estilo C.
Oktalist
12
Hice un punto de referencia y la acumulación fue en realidad más rápida que una secuencia de cadenas O (n).
kirbyfan64sos
10

¿Qué pasa con una solución simple y estúpida?

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;
}
usuario3545770
fuente
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();
}
Mamba negro
fuente
7

Me gusta usar esta acumulación de una sola línea (sin delimitador final):

std::accumulate(
    std::next(elems.begin()), 
    elems.end(), 
    elems[0], 
    [](std::string a, std::string b) {
        return a + delimiter + b;
    }
);
Sasa Milenkovic
fuente
2
Tenga cuidado cuando esté vacío.
Carlos Pinzón
6

Especialmente con colecciones más grandes, desea evitar tener que verificar si todavía está agregando el primer elemento o no para asegurarse de que no haya separadores finales ...

Entonces, para la lista vacía o de un solo elemento, no hay iteración en absoluto.

Los rangos vacíos son triviales: devuelve "".

Los elementos individuales o multielemento se pueden manejar perfectamente mediante 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;
         });
};

Ejecución de muestra ( requiere C ++ 14 ): http://cpp.sh/8uspd

xtofl
fuente
3

Una versión que usa 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ᵩ
fuente
2

Aquí hay otro que no agrega el delimitador después del último elemento:

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

El uso de parte de esta respuesta a otra pregunta le da un esto unido, basado en un separador sin una coma al final,

Uso:

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

Código:

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

Solución un poco larga, pero no se usa std::ostringstreamy no requiere un truco para eliminar el último delimitador.

http://www.ideone.com/hW1M9

Y el codigo:

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

Esto es lo que uso, simple y flexible.

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

utilizando:

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

salida:

a!@#bbb!@#c
usuario1438233
fuente
-1

solo agrega !! String s = "";

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

prueba esto, pero usando vector en lugar de lista

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
fuente