Pular para o conteúdo principal

STL não tem split - e por que isso não é importante!

Orientação a objetos é um paradigma de programação muito usado nas linguagens consideradas modernas, tais como: C++, Java, C#, entre outras... Em teoria, um código orientado a objetos pode ser facilmente reusado e entendido. Porém, eu gostaria de analisar um caso neste post. Então, vamos fazer um pequeno exercício por aqui.

Aviso: Orientação a objetos (OO) é um paradigma de programação que envolve classes e objetos, polimorfismo, herança, e uma série de outros recursos. Ter ou usar objetos não significa que seja orientado a objetos em sua plenitude. Uma das premissas da OO é que o estado interno dos objetos deve ser inacessível externamente - isso se chama encapsulamento. No entanto, encapsulamento do estado sempre é bom? Fica aí algo para pensarmos, não é mesmo?

São constantes as reclamações que C++ não tem funções básicas na STL como, por exemplo, string split. Então, vamos implementar uma por aqui.



Para simplificar, não vamos nos preocupar muito com questões de eficiência e tratamento de erros, afinal esta (ainda) não é uma biblioteca real, é só um exercício. :)

[code language="cpp"]
struct nstring {
nstring(const std::string &s) : _data(s) { }
const string &data() const { return _data; }
private:
string _data;
};
[/code]

Temos a nossa classe que encapsula uma string. Agora vamos criar um método split:

[code language="cpp"]
const vector<nstring> split(char c) {
vector<nstring> ret;
int old = 0;
for(int i = 0; i < _data.size(); ++i) {
if( _data[i] == c ) {
ret.push_back( _data.substr(old, i-old) );
old=i+1;
}
}
if( old < _data.size() ) { ret.push_back( _data.substr(old) ); }
return ret;
}
[/code]

Simples e direta. Pega a string contida na nossa classe nstring e retorna um vetor com n strings resultante da separação.

Vamos fazer agora uma implementação não orientada a objetos, apenas uma função acessando diretamente o estado que na OO seria encapsulado certamente:

[code language="cpp"]
vector<string> split(const string &data, char c) {
vector<string> ret;
auto old = cbegin(data);
for(auto it = old; it != cend(data); ++it) {
if( *it == c ) {
ret.emplace_back( old, it );
old=it+1;
}
}
if( old != cend(data) ) {
ret.emplace_back( old, cend(data) );
}
return ret;
}
[/code]

Uma primeira coisa que pode-se pensar é: dentro da classe fica mais organizado, inclusive por causa dos recursos da IDE (como por exemplo: o IntelliSense ou o Visual Assist), porém vamos imaginar o seguinte:

Essa implementação ficou legal. Vamos colocar na nossa classe wide_string.  Logo, teríamos duas alternativas se fosse OO:
1. Duplicar a implementação (copy-and-paste programming);
2. Criar uma classe base que faça o split e outras coisas mais.

[code language="cpp"]
template<typename T, typename Base>
struct nstring_common {
vector<Base> split(typename T::value_type c) {
vector<Base> ret;
const T &s_data = data();
// .. same as before ..
}
};

template<typename T>
struct nstring_basic : public nstring_common<T, nstring_basic<T>> {
nstring_basic(const T &value) { _data = value; }
const T &data() const override { return _data; }
private:
T _data;
};

using nstring = nstring_basic<string>;
using nwstring = nstring_basic<wstring>;
[/code]

No caso da versão não OO, não precisamos fazer nada,  apenas deixá-la templatized! Ou seja, genérica:

[code language="cpp"]
template<typename T>
vector<T> split(T &data, typename T::value_type c) {
vector<T> ret;
auto old = cbegin(data);
for(auto it = old; it != cend(data); ++it) {
if( *it == c ) {
ret.emplace_back( old, it );
old=it+1;
}
}
if( old != cend(data) ) {
ret.emplace_back( old, cend(data) );
}
return ret;
}
[/code]

Só que eu gostei tanto desta versão de split que eu poderia usá-la para um outra estrutura com requisitos compatíveis, ao invés da string, porque não utilizar com um vetor?

Agora eu tenho outras duas alternativas:
1. Novamente duplicar a implementação (copy-and-paste programming);
2. Criar uma classe base sequential_buffer implementando o split.

Neste caso a implementação fica de lição de casa!

No caso da versão não OO, não precisamos fazer nada:

[code language="cpp"]
using my_type = vector<int>;
my_type x = {1, 2, 3, 4, 5, 6, 4, 7, 8, 4, 9, 10};
vector<my_type> xs = split(x, 4);
[/code]

Agora vamos supor que eu estou tão animado com a minha implementação de split que eu quero definir um predicado.

Vou fazer só na minha versão genérica, pois já mostrei um dos motivos da STL não ser orientada a objetos: Ser extensível mais facilmente!

[code language="cpp"]
template<typename T>
vector<T> split(T &data, typename T::value_type c,
std::function<bool(typename T::value_type, typename T::value_type)> pred) {
vector<T> ret;
auto old = cbegin(data);
for(auto it = old; it != cend(data); ++it) {
if( pred(*it, c) ) {
//...
}
[/code]

Primeiro vamos manter o comportamento anterior, onde a separação é baseada no número 4. Para isto, utilizamos um predicado de igualdade. No caso, o predicado é uma função que retorna um bool - ela permitirá a mudança do comportamento do algoritmo de split.

[code language="cpp"]
my_type x = {1, 2, 3, 4, 5, 6, 4, 7, 8, 4, 9, 10};
vector<my_type> xs = split(x, 4, [](int a, int b) {
return( a == b );
});
print(xs);
[/code]

Agora, com uma pequena alteração no predicado, e tornar o exemplo mais divertido, vamos separar os elementos pares e deixar os ímpares!

[code language="cpp"]
my_type y = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
vector<my_type> ys = split(y, 2, [](int a, int b) {
return( a % b == 0 );
});
[/code]

Finalizando, o intuito aqui não é criar polêmica ou dizer que a orientação a objetos é ruim e a programação genérica é boa. Mas sim, mostrar como um paradigma, quando levado a risca, de forma rígida, poderá mais atrapalhar do que ajudar.

Fontes:
https://github.com/SimplyCpp/examples/blob/master/oo1.cpp
https://github.com/SimplyCpp/examples/blob/master/noo1.cpp
https://github.com/SimplyCpp/examples/blob/master/oo2.cpp
https://github.com/SimplyCpp/examples/blob/master/noo2.cpp
https://github.com/SimplyCpp/examples/blob/master/noo3.cpp
https://github.com/SimplyCpp/examples/blob/master/noo4.cpp

Comentários

Postagens mais visitadas deste blog

Mestre Iota

Iota é a nona letra do alfabeto grego, ela é equivalente à letra i do nosso alfabeto. Por convenção ou hábito, utilizamos a letra i na programação para indicar algum tipo de incrementador, como por exemplo, em um for-loop . https://gist.github.com/fabiogaluppo/a23894ae743f7dd29274 Curiosamente, iota , como identificador, também é utilizado na programação para indicar uma sequência finita e consecutiva de números inteiros, como por exemplo, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]. Inclusive, originalmente na STL existia a função iota , inspirada pela linguagem de programação APL , você pode conferir neste link: http://www.sgi.com/tech/stl/iota.html

Valores Aleatórios Simplificados

A partir do C++ 11, foi introduzido o header <random>  com diversos facilitadores para suporte de geração de números aleatórios. A produção destes números é feita através da combinação de duas categorias de objetos: os geradores e os distribuidores. Os geradores, são responsáveis pela geração dos números, e os distribuidores são responsáveis pela transformação dos números gerados em algum tipo de distribuição de probabilidade.  Como por exemplo, uma distribuição normal (aquela da Gaussiana) ou uma distribuição de Pareto (aquela do 80-20). As opções não faltam, como você pode ver nas referências, por exemplo:   http://www.cplusplus.com/reference/random/ ou  http://en.cppreference.com/w/cpp/header/random .

Policy-based design: log writer

Policy-based design Vamos neste artigo dar mais uma pincelada no Policy-based design . Vamos fazer como exemplo uma classe de log. Como este é só um exemplo, não vamos considerar múltiplos parâmetros no log, mas somente uma string, assim não fugiremos do assunto. Uma das coisas mais importantes neste tipo de design é o desacoplamento. Ele é uma excelente alternativa ao uso de interfaces por duas razões: Não gera chamadas virtuais (ou um nível de indireção em tempo de execução) Duck typing ( https://pt.wikipedia.org/wiki/Duck_typing ) Eu gosto bastante desse tipo de design, já usado aqui: http://simplycpp.com/2016/02/05/leitura-de-configuracao-em-c/