paulo1205
(usa Ubuntu)
Enviado em 05/10/2017 - 22:04h
Andei pensando em alguns comentários a mais sobre o artigo, em pontos específicos de alguns itens, que vão além da simples lembrança de que muito do que foi considerado “
pythonic ” não é original de Python (mas que é verdade, e se aplica a todos os itens).
— Laços de repetição iterando sobre coleções: A rigor, C++ já tinha uma função para iteração sobre coleções (
std::for_each ) desde o padrão de 1998, mas não restam dúvidas de que a nova sintaxe é mais conveniente. Porém, é complicado dizer que a inspiração veio de Python, uma vez que tantas outras linguagens têm mecanismos semelhantes.
Interessante no artigo que o autor termina falando que gostaria de ter em C++ algo como
xrange () do Python 2, renomeado para
range () no Python 3 (o antigo
range () do Python 2 deixou de existir), no C++.
Antes de entrar no mérito, cabe lembrar que
range ()/
xrange () é um recurso da biblioteca, não da linguagem propriamente dita. A parte da linguagem, tanto em Python quanto em C++, é um comando, chamado
for em ambas, permitir o uso de um protocolo para iteração sobre objetos de qualquer classe que se interesse por servir a essa finalidade.
A construção como “
for i in range(incio, fim, passo) ” no Python funciona do seguinte modo: a função
range () cria um objeto da classe
range , que, por sua vez, tem métodos para gerar um iterador, que permite acesso sequencial aos elementos da faixa. Assim sendo, um laço de repetição que tem como controle um objeto desse tipo depende da chamada a métodos padronizados para fazer a iteração.
É óbvio que tal coisa pode ser representada em C++ também (tenho um programa ilustrativo que pretendo postar posteriormente). Mas se objetivo de uma classe
à la range é principalmente para controle de laços de repetição, então dificilmente um programador C++ teria interesse em usá-la. A forma tradicional de fazer repetições ou sequências com
for — algo como “
for(int i=inicio; i<fim; i+=passo) ” — é muito mais eficiente, por não ter de lidar com objetos nem invocar métodos ou funções. A representação como a de Python não economizaria nem mesmo muita coisa de digitação: “
for(auto i: xrange(n)) ” tem apenas um caráter a menos que “
for(auto i=0; i<n; i++) ”, e empata se, no segundo caso, se usar “
int ” em vez de “
auto ”, como é mais comum; num caso geral, “
for(auto i: xrange(inicio, fim, passo)) ” tem quatro caracteres
a mais do que “
for(auto i=inicio; i<fim; i+=passo) ”.
— Dedução automática de tipos: Sendo C++ uma linguagem de tipagem estática mas que, ao mesmo tempo, permite a criação de tipos novos e composição desses tipos com graus de complexidade virtualmente ilimitados, tanto por herança tradicional de OOP quanto, principalmente, por mecanismos de GP, é bem razoável criar um instrumento que transfira para o compilador a tarefa de descobrir o tipo de um objeto complexo, desde que tal tipo possa ser obtido de modo não ambíguo.
Não fosse assim (e não era no padrão de 1998; e ainda o será se a composição de tipos resultar num em ambiguidade), um programador que quisesse usar as composições complexas teria de conhecer os pormenores de cada tipo criado diretamente ou por derivação, e usar declarações gigantescas que expusessem toda complexidade dessas composições a cada vez que quisesse usar um objeto desse tipo.
A forma de Python de evitar esse tipo de problema é usar tipagem dinâmica. Contudo, essa escolha tem lá seus ônus, incluindo o menor desempenho. Em C++, o programa compilado não carrega ônus de descobrir tipos, permitindo execução muito eficiente, mas esse custo não some: ele é transferido para o momento da compilação (o que explica, em parte, a percepção de que a compilação de programas em C++ é mais lenta do que a de códigos compráveis em C, Pascal e outras linguagens semelhantes).
— Inicialização uniforme: Não me parece que a motivação tenha sido externa. C++ (e C) sempre permitiram a inicialização de alguns tipos agregados de dados por meio de listas de inicialização (particularmente
arrays nativos e estruturas). Sendo classes uma forma de agregação de dados, e mais ainda quando existem muitas classes cuja função é justamente substituir os tipos agregados nativos com interfaces mais convenientes (e.g.
std::vector ,
std::list ,
std::string etc.), é razoável desejar que listas de inicialização pudessem ser usadas com esse novos tipos, como eram com os antigos.
— Lambdas: Os lambdas de C++ são mais versáteis que os de Python, pois permitem escrever funções tão complexas quanto se queria, ao passo que Python as limita a apenas uma expressão, pois ficaria difícil ter lambdas mais complexos sem interferir no comportamento que a linguagem exige com relação à indentação de código.
O irônico é que, apesar da limitação que têm em Python, sua comunidade se refere ao recurso como “funções lambda”, enfatizando os momentos em que os lambdas são usados. Em C++, embora a função definida possa ter a complexidade que se quiser, muita gente põe o foco no
loco em que a função é definida, que é num contexto de expressão, e acaba chamado de “expressão lambda”.
Como eu acho que as duas coisas estão corretas, em ambas as linguagens, prefiro chamar só de lambda.
— Algoritmos padronizados: Outra obra que me parece interna. A biblioteca padrão já trazia algoritmos padronizados desde 1998. O que se fez agora foi apenas incluir alguns algoritmos novos e atualizar alguns dos antigos, adaptando-os ao uso de novos recursos, como referências para
rvalues e
templates com argumentos variáveis.
— Empacotamento de parâmetros: O autor do artigo fez colocações corretas, mas acho não soube mostrar isso nos exemplos que trouxe. Na verdade, os exemplos acabam borrando a distinção que ele soube colocar bem e em poucas palavras.
Penso que seriam mais produtivos exemplos que se aproximassem mais de como os programadores de cada uma das linguagens efetivamente usariam parâmetros empacotados de num programa real do dia-a-dia. Tentei imaginar um caso assim, que transcrevo abaixo, em três versões: Python 3, C++14 e C++17.
#!/usr/bin/python3
import sys
def pack_to_str(*args):
// Os vários argumentos viram uma única tupla dentro da função.
if len(args)==0:
return "\n"
elif len(args)==1:
return str(*args)+"\n" # expande a tupla
return str(args[0])+", "+pack_to_str(*args[1:])
# ^^^^^^^ ^^^^^^^^^
# usa 1º argumento expande N-1 argumentos em nova chamada
# Modo de uso
sys.stdout.write(pack_to_str())
sys.stdout.write(pack_to_str(1))
sys.stdout.write(pack_to_str(1, "two"))
sys.stdout.write(pack_to_str(1, "two", 3))
// C++14
#include <iostream>
#include <string>
using namespace std;
// Função auxilar: a biblioteca padrão só define to_string()
// para tipos numéricos. Então eu providencio uma para strings.
string to_string(const string &s){ return s; }
string to_string(char c){ return string(1, c); }
std::string pack_to_str(){ return "\n"s; }
template <class Type> std::string pack_to_str(const Type &arg){
return to_string(arg)+"\n"s;
}
// N argumentos: um não-empacotado, N-1 empacotados.
template <class Type, class... OtherTypes>
std::string pack_to_str(const Type &arg, const OtherTypes... &other_args){
return to_string(arg)+", "s+pack_to_str(other_args);
// Na próxima chamada, o segundo argumento vira primeiro (não-empacotado).
}
int main(){
// Exemplo de uso:
cout << pack_to_str();
cout << pack_to_str(1);
cout << pack_to_str(1, "two"s);
cout << pack_to_str(1, "two"s, '3');
}
// C++17, com folding e constexpr if.
#include <iostream>
#include <string>
#include <utility>
using namespace std;
string to_string(const string &s){ return s; }
string to_string(char c){ return string(1, c); }
// Função embute vírgula ao final da string.
template <class C> string concat_helper(const C &arg){
return to_string(arg)+", "s;
}
template <class... C> string my_func(const C &... args){
// Dois constexpr ifs (e seus elses), que são testados e resolvidos
// durante a compilação, não durante a execução.
if constexpr(sizeof...(args)==0)
return "\n"s;
else if constexpr(sizeof...(args)==1)
return to_string(args...)+'\n';
else{
string s;
(s+=...+=concat_helper(args)); // Folding.
s.replace(s.length()-2, 2, "\n"s); // Troca a vírgula a mais por quebra de linha.
return s;
}
}
int main(){
cout << my_func();
cout << my_func(1);
cout << my_func(1, "two"s);
cout << my_func(1, "two"s, '3');
}