As aulas provavelmente foram a primeira coisa que o Stroustrup adicionou na década de 1980, marcando o nascimento de C ++. Se nos imaginarmos como arqueólogos estudando C ++ antigo, uma peça de evidência indireta que apoia a teoria seria a palavra -chave ‘essa’, que ainda é um ponteiro em C ++, sugerindo que ela foi introduzida antes das referências!
Publicamos e traduzimos este artigo com a permissão do detentor de direitos autorais. O autor é Kelbon.
Esse não é o ponto, no entanto. Vamos olhar para a evolução de C ++ desde então: o idioma e seu desenvolvimento de paradigmas, a seleção natural das melhores práticas e ocasionais “descobertas significativas”. Isso nos ajudará a entender como o idioma, uma vez chamado oficialmente de “C com classes” (agora é mais um meme), evoluiu.
No final deste artigo (spoiler), tentaremos transformar o C ++ em uma linguagem funcional em algumas etapas simples.
Primeiro, veremos o uso básico das aulas:
class Foo : public Bar { // inheritance
public:
int x;
};
// it's exactly the same, but struct
struct Foo : Bar {
int x;
};
Mesmo esse exemplo simples mostra que OOP, encapsulamento, herança e outros conceitos relacionados foram os paradigmas dominantes quando as aulas foram introduzidas. Foi decidido que a classe seria herdada em particular por padrão, assim como seus membros de dados. Experiência prática mostrou isso:
- A herança privada é uma criatura extremamente rara, quase nunca encontrada no código do mundo real;
- Você sempre tem algo público, mas nem sempre algo privado.
Originalmente, o estilo C. struct
Não tinha as capacidades de uma classe – nenhuma função de membro, construtores ou destruidores. Mas hoje, a única diferença entre um struct
e class
Em C ++ se resume a esses dois parâmetros por padrão. Isso significa que sempre que usamos um class
Em nosso código, provavelmente estamos adicionando outra linha extra. Dando struct
Todas essas capacidades foram apenas o primeiro passo das classes tradicionais.
Mas o class
A palavra -chave tem muito mais definições! Vamos dar uma olhada em todos eles!
Em um modelo:
template // same as template
void foo() { }
Talvez seu único objetivo em 2K22 seja confundir o leitor, embora alguns o usem para salvar até três caracteres. Bem, não vamos julgá -los.
Em um modelo, mas não como inútil (para declarar parâmetros de modelo de modelo):
// A function that takes a template with
// one argument as a template argument
template class T>
void foo() { }
// since C++17
template typename T>
void foo() { }
// it's funny, but we shouldn't do that
template class T> // compilation error
void foo() { }
Em C ++ 17, esse recurso é obsoleto, então agora podemos escrever typename
sem problemas. Como você pode ver, estamos se afastando cada vez mais class
…
Leitores familiarizados com C ++ obviamente lembre -se do enum
aula! Como não há como substituí -lo, como podemos evitá -lo?
Você não vai acreditar nisso, mas os seguintes trabalhos:
enum struct Heh { a, b, c, d };
Então, é isso que temos: no momento, não precisamos usar o class
Palavra -chave em C ++, o que é engraçado.
Mas espere, há mais! Graças a Deus C ++ não está ligado a nenhum paradigma, então a morte de class
muda quase nada. O que estava acontecendo com outros ramos de programação?
Em meados dos anos 90, o mundo do C ++ testemunhou de repente duas grandes descobertas: a Biblioteca de Modelos Padrão (STL) e o tipo metaprogramação.
Ambos eram altamente funcionais. Eles provaram ser bastante úteis: o uso de modelos de função gratuito em vez de funções de membros nos algoritmos STL resulta em maior conveniência e flexibilidade. O begin
Assim, end
Assim, size
e swap
As funções são particularmente dignas de nota. Como não são funções membros, eles podem ser facilmente adicionados a tipos de terceiros e trabalham com tipos fundamentais, como matrizes C, no código do modelo.
A metaprogramação do modelo é puramente funcional porque não possui estado ou mutabilidade global, mas possui recursão e mônadas.
Funções e funções de membro também parecem algo obsoleto em comparação com Lambdas (objetos funcionais). Afinal, uma função é essencialmente um objeto funcional sem um estado. E uma função de membro é um objeto funcional sem um estado que requer uma referência ao seu tipo declarado.
Parece que agora acumulamos motivos suficientes para transformar o C ++ em uma linguagem funcional … tudo bem, vamos começar!
Se pensarmos sobre isso, tudo o que está faltando é um substituto para funções, funções de membro e curry embutido, o que é relativamente fácil de implementar no C ++ moderno.
Vamos empunhar uma equipe mágica e nos esconder em uma túnica de metâmetros:
// this type only stores other types
template
struct type_list;
// you can find its implementation at the link,
// the main feature is to take the function signature by type
template
struct callable_traits;
Agora, vamos declarar o tipo de fechamento que armazenará qualquer lambda e fornecer as operações necessárias no horário de compilação:
template
struct closure;
template
struct closure<:type_list f="">> {
F f; // we store the lambda!
// We don't inherit here because it might be
// a pointer to a function!
// see below
};
O que está acontecendo aqui? Existe apenas um closure
Especialização, que é onde está a lógica principal. Vamos ver abaixo como type_list
com a assinatura e o tipo de função chega lá.
Vamos para a lógica principal.
Primeiro, precisamos ensinar o lambda a ser chamado …
R operator()(Args... args) {
// static_cast, because Args... are independent template arguments here
// (they're already known in the closure type)
return f(static_cast(args)...);
}
Ok, isso foi fácil, agora vamos adicionar um pouco de curry:
// an auxiliary free function that we'll remove later on
template
auto make_closure(T&& value) {
return closure>>(std::forward(value));
}
// We learn to detect the first type in the parameter package
// and issue a "type-error" if there are 0 types
template
struct first : std::type_identity<:false_type> {
};
template
struct first : std::type_identity {
};
// within closure
auto operator()(first_t value) requires(sizeof...(Args) > 1)
{
return [&](type_list)
{
return make_closure(
std::bind_front(*this, static_cast&&>(value))
);
}
(type_list{});
}
Essa parte requer um pouco mais de explicação … então, se recebemos um argumento, e a função não pode ser chamada com apenas uma, assumimos que está cambaleando. Nós “realmente” pegamos o tipo especificado primeiro na assinatura.
Retornamos o Lambda que leva um tipo menos e memorizou o primeiro argumento.
Nosso Lambda está basicamente pronto agora. O toque final permanece: e se uma função for chamada com apenas um argumento? Como curtimos isso? É aí que entra a filosofia.
O que é uma função curry com um argumento, dado que os idiomas funcionais não têm estado global? A resposta não é óbvia, mas é simples. O valor! Qualquer chamada para essa função é simplesmente o valor do tipo resultante, e é sempre o mesmo!
Portanto, podemos adicionar um operador de elenco ao tipo resultante, mas somente quando temos 0 argumentos!
// in closure
operator R()
requires(sizeof...(Args) == 0) {
return (*this)();
}
Espere! Não estamos esquecendo algo? Como o usuário deve usar isso? Eles precisam especificar o tipo, não é? O C ++ cuidou disso! CTAD (dedução do argumento do modelo de classe (HEH)) nos permite escrever uma dica para um compilador como deduzir um tipo. Aqui está o que parece:
template
closure(F&&) -> closure::func_type, std::decay_t>>;
Podemos finalmente aproveitar o resultado:
// The replacement for global functions:
#define fn constexpr inline closure
void foo(int x, float y, double z) {
std::cout << x << y << z << '\n';
}
fn Foo = foo; // the lambda could be here, too
int main() {
// currying
Foo(10, 3.14f, 3.1); // just a normal call
Foo(10)(3.14f, 3.1); // currying by one argument and then calling
Foo(10)(3.14f)(3.1); // currying up to the end
// closure returning closure
closure hmm = [](int a, float b) {
std::cout << a << '\t' << b;
return closure([](int x, const char* str) {
std::cout << x << '\t' << str;
return 4;
});
};
// First two arguments are for hmm, second two are for the closure it returns
hmm(3)(3.f)(5)("Hello world");
// we also support template lambdas/overloaded functions
// via this auxiliary function
auto x = make_closure([](auto... args) {
(std::cout << ... << args);
return 42;
});
// This is certainly useful if you've ever tried to capture
// an overloaded function differently
auto overloaded = make_closure(overloaded_foo);
}
O código completo com todas as sobrecargas (para desempenho) – esse problema é resolvido em C ++ 23 com “dedução disso”.
Uma versão com type erasure
Para uso conveniente de tempo de execução, está em exemplos.