Por que matrizes de comprimento variável não fazem parte do padrão C ++?

326

Não usei muito C nos últimos anos. Quando li essa pergunta hoje, deparei-me com uma sintaxe C com a qual não estava familiarizada.

Aparentemente, em C99, a seguinte sintaxe é válida:

void foo(int n) {
    int values[n]; //Declare a variable length array
}

Parece um recurso bastante útil. Houve alguma discussão sobre como adicioná-lo ao padrão C ++ e, se sim, por que ele foi omitido?

Algumas razões possíveis:

  • Cabeludo para os fornecedores de compiladores implementarem
  • Incompatível com alguma outra parte do padrão
  • A funcionalidade pode ser emulada com outras construções C ++

O padrão C ++ afirma que o tamanho da matriz deve ser uma expressão constante (8.3.4.1).

Sim, é claro que percebo que no exemplo de brinquedo se poderia usar std::vector<int> values(m);, mas isso aloca memória da pilha e não da pilha. E se eu quiser uma matriz multidimensional como:

void foo(int x, int y, int z) {
    int values[x][y][z]; // Declare a variable length array
}

a vectorversão se torna bastante desajeitada:

void foo(int x, int y, int z) {
    vector< vector< vector<int> > > values( /* Really painful expression here. */);
}

As fatias, linhas e colunas também serão potencialmente espalhadas por toda a memória.

Olhando para a discussão comp.std.c++, fica claro que essa questão é bastante controversa com alguns nomes muito pesados ​​nos dois lados do argumento. Certamente não é óbvio que a std::vectoré sempre uma solução melhor.

Andreas Brinck
fonte
3
Por curiosidade, por que ele precisa ser alocado na pilha? Você tem medo de problemas de desempenho de alocação de heap?
22620 Dimitri C.
32
@ Dimitri Na verdade não, mas não há como negar que a alocação de pilha será mais rápida que a alocação de heap. E, em alguns casos, isso pode importar.
Andreas Brinck
11
A principal vantagem das matrizes de comprimento variável é que todos os dados estão próximos; assim, quando você percorre essa matriz, lê e grava bytes um ao lado do outro. Seus dados são buscados no cache e a CPU pode trabalhar com ele sem buscar e enviar os bytes para / da memória.
Calmarius
4
Matrizes de comprimento variável também podem ser usadas para substituir constantes do pré-processador por variáveis ​​estáticas. Também em C, você não tem outras opções para o VLA, e às vezes é necessário escrever código C / C ++ portátil (compatível com os dois compiladores).
Yury
2
como um aparte, parece clang ++ permite VLAs.
user3426763

Respostas:

204

Recentemente, houve uma discussão sobre isso na usenet: Por que não há VLAs em C ++ 0x .

Eu concordo com as pessoas que parecem concordar que ter que criar uma grande variedade potencial na pilha, que geralmente tem apenas pouco espaço disponível, não é bom. O argumento é que, se você souber o tamanho de antemão, poderá usar uma matriz estática. E se você não souber o tamanho de antemão, escreverá um código não seguro.

Os VLAs C99 podem fornecer um pequeno benefício de poder criar matrizes pequenas sem desperdiçar espaço ou chamar construtores para elementos não utilizados, mas eles introduzirão mudanças bastante grandes no sistema de tipos (você precisa especificar tipos dependendo dos valores de tempo de execução - isso ainda não existe no C ++ atual, exceto newos especificadores de tipo de operador, mas eles são tratados especialmente, para que o tempo de execução não escape do escopo do newoperador).

Você pode usar std::vector, mas não é o mesmo, pois usa memória dinâmica, e fazê-lo usar o próprio alocador de pilha não é exatamente fácil (o alinhamento também é um problema). Também não resolve o mesmo problema, porque um vetor é um contêiner redimensionável, enquanto os VLAs são de tamanho fixo. A proposta C ++ Dynamic Array visa apresentar uma solução baseada em biblioteca, como alternativa a um VLA baseado em linguagem. No entanto, não fará parte do C ++ 0x, tanto quanto eu sei.

Johannes Schaub - litb
fonte
22
+1 e aceito. Um comentário, no entanto, acho que o argumento de segurança é um pouco fraco, pois existem muitas outras maneiras de causar estouros de pilha. O argumento de segurança pode ser usado para apoiar a posição de que você nunca deve usar recursão e que deve alocar todos os objetos do heap.
Andreas Brinck
17
Então, você está dizendo que, como existem outras maneiras de causar estouros de pilha, é melhor incentivarmos mais delas?
jalf
3
@ Andreas, concordou sobre a fraqueza. Mas, para recursão, são necessárias muitas chamadas até que a pilha seja consumida e, se esse for o caso, as pessoas usariam a iteração. Como algumas pessoas no segmento da usenet dizem, no entanto, esse não é um argumento contra os VLAs em todos os casos, pois às vezes você definitivamente conhece um limite superior. Mas, nesses casos, pelo que vejo uma matriz estática pode ser igualmente suficiente, já que de qualquer maneira não desperdiçaria muito espaço (se assim fosse , seria necessário perguntar se a área da pilha é grande o suficiente novamente).
Johannes Schaub - litb 11/12/09
10
Observe também a resposta de Matt Austern nesse segmento: a especificação da linguagem dos VLAs provavelmente seria consideravelmente mais complexa para C ++, devido às correspondências mais estritas do tipo em C ++ (exemplo: C permite atribuir T(*)[]a a T(*)[N]- em C ++, isso não é permitido, pois C ++ não sabe sobre "compatibilidade de tipos" - requer correspondências exatas), parâmetros de tipos, exceções, con- destrutores e destrutivos. Não tenho certeza se os benefícios dos VLAs realmente compensariam todo esse trabalho. Mas nunca usei VLAs na vida real, então provavelmente não conheço bons casos de uso para eles.
Johannes Schaub - litb
1
@AHelps: Talvez o que seria melhor para isso seria um tipo que se comporte de alguma forma, vectormas exija um padrão de uso LIFO fixo e mantenha um ou mais buffers alocados estaticamente por thread, geralmente dimensionados de acordo com a maior alocação total que o thread tiver já usado, mas que poderia ser explicitamente aparado. Uma "alocação" normal exigiria, no caso comum, nada mais que uma cópia do ponteiro, subtração ponteiro do ponteiro, comparação de números inteiros e adição do ponteiro; a desalocação exigiria simplesmente uma cópia do ponteiro. Não é muito mais lento que um VLA.
precisa
217

(Histórico: Tenho alguma experiência na implementação de compiladores C e C ++.)

Matrizes de comprimento variável no C99 foram basicamente um passo em falso. Para dar suporte aos VLAs, o C99 teve que fazer as seguintes concessões ao bom senso:

  • sizeof xnem sempre é uma constante em tempo de compilação; o compilador às vezes deve gerar código para avaliar uma sizeofexpressão em tempo de execução.

  • Permitindo VLAs bidimensionais ( int A[x][y]) necessária uma nova sintaxe para declarar funções que levam 2D VLAs como parâmetros: void foo(int n, int A[][*]).

  • Menos importante no mundo C ++, mas extremamente importante para o público-alvo de programadores de sistemas embarcados da C, declarar um VLA significa mastigar um pedaço arbitrariamente grande da sua pilha. Este é um estouro e empilhamento garantidos da pilha. (Sempre que você declara int A[n], está implicitamente afirmando que possui 2 GB de pilha de sobra. Afinal, se você sabe que " né definitivamente menor que 1000 aqui", então basta declarar int A[1000]. Substituir o número inteiro de 32 bits npor 1000é uma admissão que você não tem idéia de qual deve ser o comportamento do seu programa.)

Ok, então vamos falar sobre C ++ agora. Em C ++, temos a mesma forte distinção entre "sistema de tipos" e "sistema de valores" que o C89 possui ... mas realmente começamos a confiar nele de maneiras que C não tem. Por exemplo:

template<typename T> struct S { ... };
int A[n];
S<decltype(A)> s;  // equivalently, S<int[n]> s;

Se nnão fosse uma constante em tempo de compilação (ou seja, se Afosse do tipo modificado de forma variável), qual seria o tipo de terra S? O Stipo também seria determinado apenas em tempo de execução?

Que tal isso:

template<typename T> bool myfunc(T& t1, T& t2) { ... };
int A1[n1], A2[n2];
myfunc(A1, A2);

O compilador deve gerar código para alguma instanciação de myfunc. Como deve ser esse código? Como podemos gerar estaticamente esse código, se não sabemos o tipo de A1em tempo de compilação?

Pior, e se, no tempo de execução n1 != n2, isso acontecer !std::is_same<decltype(A1), decltype(A2)>()? Nesse caso, a chamada para myfunc nem deve ser compilada , porque a dedução do tipo de modelo deve falhar! Como poderíamos emular esse comportamento em tempo de execução?

Basicamente, o C ++ está se movendo na direção de empurrar cada vez mais decisões para o tempo de compilação : geração de código de modelo, constexpravaliação de funções e assim por diante. Enquanto isso, o C99 estava ocupado inserindo decisões tradicionalmente em tempo de compilação (por exemplo sizeof) no tempo de execução . Com isso em mente, realmente faz sentido gastar algum esforço tentando integrar VLAs no estilo C99 no C ++?

Como todos os outros respondentes já apontaram, o C ++ fornece muitos mecanismos de alocação de heap ( std::unique_ptr<int[]> A = new int[n];ou std::vector<int> A(n);os mais óbvios) quando você realmente deseja transmitir a idéia "Não faço ideia de quanta RAM eu possa precisar". E o C ++ fornece um modelo bacana de tratamento de exceções para lidar com a situação inevitável de que a quantidade de RAM necessária é maior que a quantidade de RAM que você possui. Mas espero que esta resposta lhe dê uma boa idéia de por que os VLAs no estilo C99 não eram adequados para C ++ - e nem mesmo para C99. ;)


Para obter mais informações sobre o assunto, consulte N3810 "Alternativas para extensões de matriz" , artigo de outubro de 2013 de Bjarne Stroustrup sobre VLAs. O ponto de vista de Bjarne é muito diferente do meu; O N3810 se concentra mais em encontrar uma boa sintaxe C ++ ish para as coisas e em desencorajar o uso de matrizes brutas em C ++, enquanto eu me concentrei mais nas implicações para a metaprogramação e o sistema de tipos. Não sei se ele considera as implicações da metaprogramação / sistema de tipos resolvidas, solucionáveis ​​ou meramente desinteressantes.


Uma boa publicação no blog que atinge muitos desses mesmos pontos é "Uso legítimo de matrizes de comprimento variável" (Chris Wellons, 27/10/2019).

Quuxplusone
fonte
15
Concordo que os VLAs estavam errados. O muito mais amplamente implementado, e muito mais útil, alloca()deveria ter sido padronizado no C99. Os VLAs são o que acontece quando um comitê de padrões salta à frente das implementações, e não o contrário.
Cientista louco
10
O sistema de tipos modificados de forma variável é um ótimo complemento para a IMO, e nenhum dos seus pontos violam o senso comum. (1) o padrão C não faz distinção entre "tempo de compilação" e "tempo de execução"; portanto, isso não é problema; (2) O *é opcional, você pode (e deve) escrever int A[][n]; (3) Você pode usar o sistema de tipos sem declarar realmente nenhum VLA. Por exemplo, uma função pode aceitar uma matriz do tipo modificado de forma variável e pode ser chamada com matrizes não-VLA 2-D de diferentes dimensões. No entanto, você faz pontos válidos na parte final da sua postagem.
MM
3
"declarar um VLA significa mastigar um pedaço arbitrariamente grande da sua pilha. Isso é uma sobrecarga e travamento garantidos da pilha. (Sempre que você declara int A [n], você está implicitamente afirmando que possui 2 GB de pilha de sobra" é empiricamente . falsa Eu corri um programa VLA com uma pilha muito menos do que 2 GB sem qualquer estouro de pilha.
Jeff
3
@ Jeff: Qual foi o valor máximo do nseu caso de teste e qual era o tamanho da sua pilha? Eu sugiro que você tente inserir um valor para npelo menos o tamanho da sua pilha. (E se o usuário não tiver como controlar o valor de nseu programa, sugiro que você propague o valor máximo ndireto na declaração: declare int A[1000]ou o que for necessário. Os VLAs são apenas necessários e apenas perigosos, quando o valor máximo de nnão estiver limitado por nenhuma constante pequena de tempo de compilação.)
Quuxplusone
2
Como o alloca () pode ser implementado usando tais intrínsecos, é por definição verdade que o alloca () pode ser implementado em qualquer plataforma, como uma função padrão do compilador. Não há razão para que o compilador não possa detectar a primeira instância de alloca () e providenciar para que os tipos de marcas e liberações sejam incorporados no código, e não há razão para que um compilador não possa implementar alloca () usando a pilha se isso não pode ser feito com a pilha. O que é difícil / não portátil é ter alloca () implementado em cima de um compilador C, para que ele funcione em uma ampla variedade de compiladores e sistemas operacionais.
MadScientist 12/01
26

Você sempre pode usar alloca () para alocar memória na pilha em tempo de execução, se desejar:

void foo (int n)
{
    int *values = (int *)alloca(sizeof(int) * n);
}

Ser alocado na pilha implica que ela será automaticamente liberada quando a pilha se desenrolar.

Nota rápida: Conforme mencionado na página de manual do Mac OS X para alloca (3), "A função alloca () depende da máquina e do compilador; seu uso é desaconselhável". Só para você saber.

PfhorSlayer
fonte
4
Além disso, o escopo de alloca () é a função inteira, não apenas o bloco de código que contém a variável. Portanto, usá-lo dentro de um loop aumentará continuamente a pilha. Um VLA não tem esse problema.
Sashoalm
3
No entanto, os VLAs com o escopo do bloco anexo significam que são significativamente menos úteis que alloca () com o escopo de toda a função. Considere: if (!p) { p = alloca(strlen(foo)+1); strcpy(p, foo); } Isso não pode ser feito com VLAs, precisamente devido ao escopo do bloco.
MadScientist
1
Isso não responde à pergunta do porquê dos OP . Além disso, esta é uma Csolução semelhante, e não realmente C++realista.
Adrian W
13

Em meu próprio trabalho, percebi que toda vez que desejava algo como matrizes automáticas de comprimento variável ou alloca (), realmente não me importava que a memória estivesse fisicamente localizada na pilha da cpu, apenas que ela veio de algum alocador de pilha que não teve viagens lentas para o heap geral. Então, eu tenho um objeto por thread que possui alguma memória a partir da qual ele pode enviar / armazenar buffers de tamanho variável. Em algumas plataformas, permito que isso cresça via mmu. Outras plataformas têm um tamanho fixo (geralmente acompanhado por uma pilha de CPU de tamanho fixo também porque não há mmu). Uma plataforma com a qual eu trabalho (um console de jogos portátil) possui uma preciosa pilha de CPU, pois reside em uma memória rápida e escassa.

Não estou dizendo que nunca é necessário colocar buffers de tamanho variável na pilha da CPU. Honestamente, fiquei surpreso quando descobri que isso não era padrão, pois certamente parece que o conceito se encaixa bem na linguagem. Para mim, os requisitos "tamanho variável" e "devem estar fisicamente localizados na pilha da CPU" nunca surgiram juntos. Tem sido sobre velocidade, então fiz meu próprio tipo de "pilha paralela para buffers de dados".

Eric
fonte
12

Há situações em que a alocação de memória heap é muito cara em comparação com as operações executadas. Um exemplo é a matemática matricial. Se você trabalha com matrizes pequenas, diga 5 a 10 elementos e faça muita aritmética, a sobrecarga do malloc será realmente significativa. Ao mesmo tempo, tornar o tamanho um tempo de compilação constante parece muito dispendioso e inflexível.

Eu acho que o C ++ é tão inseguro que o argumento para "tentar não adicionar mais recursos não seguros" não é muito forte. Por outro lado, como o C ++ é sem dúvida o recurso de linguagem de programação mais eficiente em tempo de execução, o que o torna ainda mais útil: as pessoas que escrevem programas críticos de desempenho usam, em larga medida, o C ++ e precisam de tanto desempenho quanto possível. Mover coisas de pilha para pilha é uma dessas possibilidades. Reduzir o número de blocos de heap é outro. Permitir VLAs como membros do objeto seria uma maneira de conseguir isso. Estou trabalhando nessa sugestão. É um pouco complicado de implementar, é certo, mas parece bastante factível.

Bengt Gustafsson
fonte
12

Parece que estará disponível no C ++ 14:

https://en.wikipedia.org/wiki/C%2B%2B14#Runtime-sized_one_dimensional_arrays

Atualização: não foi incluída no C ++ 14.

Viktor Sehr
fonte
interessante. Herb Sutter discute-o aqui em Matrizes dinâmicas : isocpp.org/blog/2013/04/trip-report-iso-c-spring-2013-meeting (esta é a referência para as informações da wikipedia)
Default
1
"Matrizes e dynarray do tamanho de tempo de execução foram movidos para a especificação técnica Array Extensions" escreveu 78.86.152.103 na Wikipedia em 18 de janeiro de 2014: en.wikipedia.org/w/…
strager
10
A Wikipedia não é uma referência normativa :) Esta proposta não foi inserida no C ++ 14.
MM
2
@ViktorSehr: Qual é o status desse wrt C ++ 17?
Einpoklum
@einpoklum Não faço ideia, uso boost :: recipiente :: static_vector
Viktor Sehr
7

Isso foi considerado para inclusão em C ++ / 1x, mas foi descartado (essa é uma correção do que eu disse anteriormente).

De qualquer forma, seria menos útil em C ++, pois já temos std::vectorque preencher essa função.

philsquared
fonte
42
Não, nós não, std :: vector não aloca dados na pilha. :)
Kos
7
"the stack" é um detalhe de implementação; o compilador pode alocar memória de qualquer lugar, desde que as garantias sobre a vida útil do objeto sejam atendidas.
MM
1
@ MM: É justo, mas na prática ainda não podemos usar em std::vectorvez de, digamos alloca(),.
Einpoklum
@einpoklum em termos de obtenção da saída correta para o seu programa, você pode. O desempenho é uma questão de qualidade de aplicação
MM
1
A qualidade de implementação do @MM não é portátil. e se você não precisa de desempenho, você não usar c ++ em primeiro lugar
pal
3

Use std :: vector para isso. Por exemplo:

std::vector<int> values;
values.resize(n);

A memória será alocada no heap, mas isso contém apenas uma pequena desvantagem de desempenho. Além disso, é aconselhável não alocar grandes blocos de dados na pilha, pois seu tamanho é bastante limitado.

Dimitri C.
fonte
4
Uma aplicação importante para matrizes de comprimento variável é a avaliação de polinômios de graus arbitrários. Nesse caso, sua "pequena desvantagem de desempenho" significa "o código é executado cinco vezes mais devagar em casos típicos". Isso não é pequeno.
AHelps
1
Por que você simplesmente não usa std::vector<int> values(n);? Ao usar resizeapós a construção, você está proibindo tipos não móveis.
LF
1

C99 permite VLA. E coloca algumas restrições sobre como declarar VLA. Para detalhes, consulte 6.7.5.2 da norma. C ++ desabilita o VLA. Mas o g ++ permite.

Jingguo Yao
fonte
Você pode fornecer um link para o parágrafo padrão que você está apontando?
26417 Vincent
0

Matrizes como essa fazem parte do C99, mas não fazem parte do C ++ padrão. como outros já disseram, um vetor é sempre uma solução muito melhor, e é provavelmente por isso que as matrizes de tamanho variável não estão no padrão C ++ (ou no padrão C ++ 0x proposto).

BTW, para perguntas sobre "por que" o padrão C ++ é como é, o grupo de notícias moderado da Usenet comp.std.c ++ é o lugar para onde ir.


fonte
6
-1 O vetor nem sempre é melhor. Frequentemente sim. Sempre não. Se você precisar apenas de uma matriz pequena, estiver em uma plataforma em que o espaço de heap seja lento e a implementação do vetor da sua biblioteca use espaço de heap, esse recurso poderá muito bem ser melhor se existir.
Patrick M
-1

Se você conhece o valor no momento da compilação, pode fazer o seguinte:

template <int X>
void foo(void)
{
   int values[X];

}

Editar: você pode criar um vetor que use um alocador de pilha (alloca), pois o alocador é um parâmetro de modelo.

Edouard A.
fonte
18
Se você conhece o valor no momento da compilação, não precisa de um modelo. Basta usar o X diretamente na sua função que não é do modelo.
Rob Kennedy
3
Às vezes, o chamador sabe em tempo de compilação e o receptor não, é para isso que servem os modelos. Obviamente, no caso geral, ninguém conhece o X até o tempo de execução.
Qwertie
Você não pode usar o alloca em um alocador STL - a memória alocada do alloca será liberada quando o quadro da pilha for destruído - é quando o método que deve alocar a memória retorna.
26412 Oliver
-5

Eu tenho uma solução que realmente funcionou para mim. Eu não queria alocar memória por causa da fragmentação em uma rotina que precisava ser executada várias vezes. A resposta é extremamente perigosa; portanto, use-a por seu próprio risco, mas aproveita a montagem para reservar espaço na pilha. Meu exemplo abaixo usa uma matriz de caracteres (obviamente, outra variável de tamanho exigiria mais memória).

void varTest(int iSz)
{
    char *varArray;
    __asm {
        sub esp, iSz       // Create space on the stack for the variable array here
        mov varArray, esp  // save the end of it to our pointer
    }

    // Use the array called varArray here...  

    __asm {
        add esp, iSz       // Variable array is no longer accessible after this point
    } 
}

Os perigos aqui são muitos, mas explicarei alguns: 1. Alterar o tamanho da variável no meio eliminaria a posição da pilha 2. Ultrapassar os limites da matriz destruiria outras variáveis ​​e possíveis códigos 3. Isso não funciona em 64 bits construir ... precisa de uma montagem diferente para essa (mas uma macro pode resolver esse problema). 4. Específico do compilador (pode haver problemas ao se mover entre compiladores). Eu não tentei, então realmente não sei.

Alan
fonte
... e se você quiser fazer isso sozinho, talvez use uma classe RAII?
Einpoklum
Você pode simplesmente usar boost :: container :: static_vector tu.
Viktor Sehr
Isso não possui equivalentes para outros compiladores que possuem mais assemblies brutos que o MSVC. O VC provavelmente entenderá que isso espmudou e ajustará seus acessos para empilhar, mas, por exemplo, o GCC você quebrará completamente - pelo menos se você usar otimizações e, -fomit-frame-pointerem particular.
Ruslan