Início Tecnologia Quem matou a classe? Uma visão forense da palavra -chave esquecida de...

Quem matou a classe? Uma visão forense da palavra -chave esquecida de C ++

7
0

 

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 beginAssim, endAssim, sizee 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.

fonte