Criei uma conta no Github e lá publiquei uma biblioteca header-only em C++ com implementação em templates de funções para split() e join(), que servem, respectivamente, para partir uma string em múltiplos pedaços a partir da identificação de partes do texto que sirvam como separadores naturais de tais pedaços, e para aglutinar numa só string uma coleção de objetos, usando um ou dois separadores diferentes.
Não se trata de nada inédito, nem difícil: existem múltiplas implementações de split e join gratuitas em C++, e é relativamente fácil fazer na mão, sem ter de recorrer a biblioteca nenhuma. Mas foi um exercício de programação genérica (ou nem tão genérica assim, dada a quantidade de especializações) que surgiu de uma pergunta postada neste fórum do Viva o Linux, que eu fui evoluindo bem aos pouquinhos ao longo de cerca de dois anos.
Para usar a biblioteca num programa, basta incluir o arquivo split.h e invocar um dos três templates de funções (ou suas especializações e adaptadores) declarados dentro do namespace org::ppires: split(), join() e basic_join() (sendo que este último provavelmente não será usado diretamente, pois existe como uma versão de back-end para join).
As funções são implementadas como templates a fim de poder trabalhar com diferentes tipos de strings. Afinal, como se sabe, os tipos std::string e std::wstring são especializações do templatestd::basic_string que usam, respectivamente, os tipos char e wchar_t para representar cada caráter que compõe suas respectivas strings. Mas além de haver outros tipos distintos de caracteres (a saber: signed char, unsigned char, char16_t, char32_t e, agora, com o C++20, char8_t), std::basic_string pode ser especializado também para usar classes que modificam operações com esses caracteres (character traits, que se pode traduzir como “características dos caracteres”) e para fazer a alocação e liberação dinâmica de memória para armazenar tais caracteres (allocators). std::string e std::wstring utilizam traits e allocators padronizados para seus respectivos caracteres, mas pode haver outros tipos de strings com outras características, e a split.h que eu fiz leva isso em consideração.
A forma geral(†) de usar todas as funções da família de split é a seguinte:
• big_string é a string a ser dividida, cujo tipo pode ser ponteiro para qualquer tipo de caracteres, qualquer especialização de std::basic_string ou qualquer especialização de std::basic_string_view.
• separator é o separador a ser considerado para separar big_string em diferentes partes. Pode ser um único caráter, uma string (nas variações ponteiro para sequência de caracteres, std::basic_string ou std::basic_string_view) ou expressões regulares (uma especialização de std::basic_regex_, desde que os tipos de caracteres e seus traits sejam compatíveis com os de big_string.
• max_parts indica a quantidade máxima de partes que podem ser retornadas pela função. O valor zero indica a quantidade mínima de partes para que não sobre nenhuma ocorrência do separador em algum dos pedaços após a separação (o que implica que partes vazias no final da string são suprimidas; por exemplo, “split("a:b:c:::", ':', 0)” vai retornar as partes "a", "b" e "c", “split("a:b:c:::", ':', 3)” vai retornar "a", "b" e "c:::", e “split("a:b:c:::", ':', 10)” vai retornar "a", "b", "c", "", "" e "").
• O valor de retorno da função é um vetor (std::vector) de strings (std::basic_string), com cada string sendo especializada com o mesmo tipo de caráter e traits que big_string e/ou separator.
O argumento max_parts pode ser omitido, o que implica usar para ele o valor zero.
Se max_parts for omitido, separator também pode ser omitida, e nesse caso será interpretada como a expressão regular que representa qualquer quantidade positiva de espaços em branco (para char, seria o equivalente a “std::regex("\\s+")”).
No caso de join(), há quatro formas gerais(‡) de usar:
str=join(inicio, fim, conector_geral, conector_final); // Caso mais geral.
str=join(colecao, conector_geral, conector_final); // Equivalente ao caso mais geral com “join(std::begin(colecao), std::end(colecao), conector_geral, conector_final)”.
str=join(inicio, fim, conector); // Equivalente ao caso mais geral com “join(inicio, fim, conector, conector)”.
str=join(colecao, conector); // Equivalente ao caso mais geral com “join(std::begin(colecao), std::end(colecao), conector, conector)”.
Sendo:
• inicio e fim devem ser iteradores de input que permitam percorrer elementos da mesma coleção. O tipo dos objetos percorridos pode ser qualquer coisa para a qual exista um operador << capaz de escrever tais objetos em um std::basic_ostream com o tipo de caracteres (char, wchar_t ou qualquer outro) aplicado à função.
• conector_geral é aquilo que vai aparecer entre os elementos que estão sendo aglutinados, exceto entre o penúltimo e o último elementos.
• conector_final é aquilo que vai aparecer entre o penúltimo e o último elementos que estão sendo aglutinados.
Assim como no caso dos objetos das coleções, os tipos dos conectores não precisam ser strings, mas qualquer coisa que possa ser disposta num objeto de classe descendente de std::ostream por meio do operador <<.
A biblioteca é uma obra em andamento. Creio que há bastante espaço para melhoria, especialmente em join(). Ainda é relativamente fácil, por exemplo, cair num caso de invocar join() com argumentos que provocam ambiguidade entre duas ou mais das diferentes especializações. Creio que o uso de concepts pode eliminar boa parte dessas ambiguidades, mas ainda não estudei o assunto o suficiente para colocar tal recurso no código (mas tenho a impressão de que seria complicado fazê-lo apenas com os recursos do C++17; talvez seja necessário esperar o C++20 sair e estar mais bem difundido).
Contribuições de eventuais interessados serão obviamente muito bem-vindas.
---------------
† Na verdade,pode haver mais parâmetros em todas as chamadas de split(). Aquelas que têm parâmetros std::basic_string_view (e que servem de base para as demais) e/ou std::basic_string permitem reconhecer diretamente o tipo de caracteres (argumento de template “char_t”) que formam a string a ser dividida e o separador, bem como seus character traits (argumento de template “char_traits_t”), mas podem precisar de informações adicionais sobre como alocar os elementos do vetor devolvido como resultado, bem como da forma de alocar espaço para cada string que reside nesses elementos. Por conta disso, essas funções têm dois parâmetros a mais, dispostos após os já mostrados: o penúltimo é um objeto que cuida da alocação e liberação de caracteres dentro de cada string, e o último é um objeto que faz a alocação de elementos do vetor retornado como resultado. Esses parâmetros têm valores default (respectivamente std::allocator<char_t>() e std::allocator<std::basic_string<char_t, char_traits_t, char_allocator_t>>()).
No caso das versões que recebem ponteiros para caracteres e nenhum dos parâmetros é std::basic_string ou std::basic_string_view, em vez de dois parâmetros opcionais a mais, são três. Além dos dois últimos, semelhantes aos descritos no parágrafo acima, há um antepenúltimo, que é um objeto que indica o tipo de character traits que devem fazer parte das strings contidas no vetor retornado pela função. O tipo default é desse parâmetro é std::char_traits<char_t>, e o valor default é std::char_traits_t<char_t>().
‡ De modo semelhante ao que ocorre com split(), também join() tem parâmetros adicionais que costumam ter valores default mas que podem receber valores para particularizar a forma e o comportamento do valor retornado. Em todas as variações, é possível especificar, após os conectroes, um parâmetro que indica o locale a ser usado, por meio de um objeto do tipo std::locale. E a maioria das variações permite também receber um último parâmetro que indica o alocador a ser usado para string de saída.
... Então Jesus afirmou de novo: “(...) eu vim para que tenham vida, e a tenham plenamente.” (João 10:7-10)