Por que as referências não são “const” em C ++?

86

Sabemos que uma "variável const" indica que uma vez atribuída, você não pode alterar a variável, assim:

int const i = 1;
i = 2;

O programa acima não será compilado; O gcc apresenta um erro:

assignment of read-only variable 'i'

Sem problemas, posso entender, mas o exemplo a seguir está além da minha compreensão:

#include<iostream>
using namespace std;
int main()
{
    boolalpha(cout);
    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;
    int const &ri = i;
    cout << is_const<decltype(ri)>::value << endl;
    return 0;
}

Produz

true
false

Esquisito. Sabemos que, uma vez que uma referência é associada a um nome / variável, não podemos alterar essa associação, alteramos seu objeto associado. Portanto, suponho que o tipo de rideve ser o mesmo que i: quando ié um int const, por que rinão é const?

Troskyvs
fonte
3
Além disso, boolalpha(cout)é muito incomum. Você poderia fazer em std::cout << boolalphavez disso.
isanae
6
Tanto para riser um "pseudônimo" indistinguível de i.
Kaz
1
Isso ocorre porque uma referência é sempre limitada ao mesmo objeto. itambém é uma referência, mas por motivos históricos você não o declara como tal de forma explícita. Assim, ié uma referência que se refere a um armazenamento e rié uma referência que se refere ao mesmo armazenamento. Mas não há diferença de natureza entre i e ri. Como você não pode alterar a vinculação de uma referência, não há necessidade de qualificá-la como const. E deixe-me afirmar que o comentário @Kaz é muito melhor do que a resposta validada (nunca explique as referências usando ponteiros, um ref é um nome, um ptr é uma variável).
Jean-Baptiste Yunès de
1
Pergunta fantástica. Antes de ver este exemplo, eu esperava is_constretornar trueneste caso também. Em minha opinião, este é um bom exemplo de por que constestá fundamentalmente ao contrário; um atributo "mutável" (a la Rust mut) parece que seria mais consistente.
Kyle Strand
1
Talvez o título deva ser alterado para "por que é is_const<int const &>::valuefalso?" ou similar; Estou lutando para ver qualquer significado para a pergunta além de perguntar sobre o comportamento dos traços de tipo
MM

Respostas:

52

Isso pode parecer contra-intuitivo, mas acho que a maneira de entender isso é perceber que, em certos aspectos, as referências são tratadas sintaticamente como ponteiros .

Isso parece lógico para um indicador :

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const* ri = &i;
    cout << is_const<decltype(ri)>::value << endl;
}

Resultado:

true
false

Isso é lógico porque sabemos que não é o objeto ponteiro que é const (pode ser feito para apontar para outro lugar), é o objeto que está sendo apontado.

Portanto, vemos corretamente a constância do próprio ponteiro retornado como false.

Se quisermos fazer o próprio ponteiroconst , temos que dizer:

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const* const ri = &i;
    cout << is_const<decltype(ri)>::value << endl;
}

Resultado:

true
true

E então eu acho que vemos uma analogia sintática com a referência .

No entanto, as referências são semanticamente diferentes dos ponteiros, especialmente em um aspecto crucial, não temos permissão para religar uma referência a outro objeto depois de ligado.

Portanto, embora as referências compartilhem a mesma sintaxe dos ponteiros, as regras são diferentes e a linguagem nos impede de declarar a própria referênciaconst desta forma:

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const& const ri = i; // COMPILE TIME ERROR!
    cout << is_const<decltype(ri)>::value << endl;
}

Presumo que não tenhamos permissão para fazer isso porque não parece ser necessário quando as regras de linguagem impedem que a referência seja reativada da mesma forma que um ponteiro poderia (se não for declarado const).

Então, para responder à pergunta:

P) Por que “referência” não é “const” em C ++?

No seu exemplo, a sintaxe faz com que o objeto seja referido constda mesma forma que faria se você estivesse declarando um ponteiro .

Certo ou errado, não temos permissão para fazer a referência em si, constmas se fôssemos, seria assim:

int const& const ri = i; // not allowed

Q) sabemos que uma vez que uma referência é ligada a um nome / variável, não podemos alterar essa ligação, mudamos seu objeto ligado. Portanto, suponho que o tipo de rideve ser o mesmo que i: quando ié a int const, por que rinão é const?

Por que o decltype()não é transferido para o objeto ao qual o referece está vinculado?

Suponho que isso seja para equivalência semântica com ponteiros e talvez também a função de decltype()(tipo declarado) seja olhar para trás, para o que foi declarado antes da ligação ocorrer.

Galik
fonte
13
Parece que você está dizendo "as referências não podem ser constantes porque são sempre constantes"?
ruakh
2
Pode ser verdade que " semanticamente todas as ações sobre eles depois de vinculados são transferidas para o objeto ao qual se referem", mas o OP esperava que isso se aplicasse decltypetambém e descobriu que não.
ruakh
10
Há muitas suposições tortuosas e analogias duvidosamente aplicáveis ​​a ponteiros aqui, o que eu acho que confunde a questão mais do que o necessário, ao verificar o padrão como o fez sonyuanyao vai direto ao ponto real: Uma referência não é um tipo cv-qualificável, portanto não pode ser const, portanto, std::is_constdeve retornar false. Eles poderiam ter usado, em vez disso, palavras que significam que deve retornar true, mas não o fizeram. É isso aí! Todas essas coisas sobre ponteiros, "eu suponho", "eu suponho", etc. não fornecem nenhuma elucidação real.
sublinhado_d
1
@underscore_d Esta é provavelmente uma pergunta melhor para o chat do que para a seção de comentários aqui, mas você tem um exemplo de "confusão desnecessária" desta forma de pensar sobre as referências? Se eles podem ser implementados dessa forma, qual é o problema em pensar neles dessa forma? (Não acho que esta pergunta conte porque decltypenão é uma operação de tempo de execução, então a ideia "em tempo de execução, as referências se comportam como ponteiros", seja correta ou não, realmente não se aplica.
Kyle Strand
1
As referências também não são tratadas sintaticamente como ponteiros. Eles têm uma sintaxe diferente para declarar e usar. Então, nem tenho certeza do que você quer dizer.
Jordan Melo
55

por que "ri" não é "const"?

std::is_const verifica se o tipo é qualificado const ou não.

Se T for um tipo qualificado const (ou seja, const ou const volatile), fornece o valor constante do membro igual a verdadeiro. Para qualquer outro tipo, o valor é falso.

Mas a referência não pode ser qualificada como const. Referências [dcl.ref] / 1

Referências qualificadas de Cv são mal formadas, exceto quando os qualificadores de cv são introduzidos através do uso de um nome de typedef ([dcl.typedef], [temp.param]) ou especificador de decltype ([dcl.type.simple]) , caso em que os qualificadores cv são ignorados.

Portanto is_const<decltype(ri)>::value, retornará falseporque ri(a referência) não é um tipo qualificado const. Como você disse, não podemos religar uma referência após a inicialização, o que implica que a referência é sempre "const", por outro lado, referência qualificada const ou referência não qualificada const pode não fazer sentido na verdade.

Songyuanyao
fonte
5
Direto ao padrão e, portanto, direto ao ponto, ao invés de toda a suposição vacilante da resposta aceita.
sublinhado_d
5
@underscore_d Esta é uma boa resposta até você reconhecer que o operador também estava perguntando por quê. "Porque está escrito" não é realmente útil para esse aspecto da questão.
simpleuser de
5
@ user9999999 A outra resposta também não responde a esse aspecto. Se uma referência não pode ser recuperada, por que não is_constretorna true? Essa resposta tenta fazer uma analogia de como os ponteiros são opcionalmente recolocáveis, ao passo que as referências não são - e, ao fazer isso, leva à autocontradição pelo mesmo motivo. Não tenho certeza se há uma explicação real de qualquer maneira, além de uma decisão um tanto arbitrária por aqueles que redigem o Padrão, e às vezes é o melhor que podemos esperar. Daí esta resposta.
sublinhado_d
2
Outro aspecto importante, penso eu, é que nãodecltype é uma função e, portanto, funciona diretamente na referência em si, e não no objeto referido. (Isso talvez seja mais relevante para a resposta "referências são basicamente ponteiros", mas ainda acho que é parte do que torna este exemplo confuso e, portanto, vale a pena mencionar aqui.)
Kyle Strand
3
Para elucidar ainda mais o comentário de @KyleStrand: decltype(name)age de forma diferente de um general decltype(expr). Então, por exemplo, decltype(i)é o tipo declarado de ique é const int, enquanto decltype((i))seria int const &.
Daniel Schepler de
18

Você precisa usar std::remove_referencepara obter o valor que procura.

std::cout << std::is_const<std::remove_reference<decltype(ri)>::type>::value << std::endl;

Para mais informações, veja esta postagem .

chema989
fonte
8

Por que as macros não const? Funções? Literais? Os nomes dos tipos?

const as coisas são apenas um subconjunto de coisas imutáveis.

Visto que os tipos de referência são apenas isso - tipos - pode ter feito algum sentido exigir o const-qualificador em todos eles para simetria com outros tipos (particularmente com tipos de ponteiro), mas isso se tornaria muito tedioso muito rapidamente.

Se C ++ tivesse objetos imutáveis ​​por padrão, exigindo a mutablepalavra-chave em qualquer coisa que você não quisesse const, então isso teria sido fácil: simplesmente não permita que os programadores adicionem mutabletipos de referência.

Como são, eles são imutáveis ​​sem qualificação.

E, uma vez que eles não são const-qualificados, provavelmente seria mais confuso para is_constum tipo de referência render true.

Acho que este é um meio-termo razoável, especialmente porque a imutabilidade é, de qualquer maneira, reforçada pelo mero fato de que não existe sintaxe para alterar uma referência.

Lightness Races in Orbit
fonte
6

Esta é uma peculiaridade / característica do C ++. Embora não pensemos nas referências como tipos, elas na verdade "assentam" no sistema de tipos. Embora isso pareça estranho (dado que quando referências são usadas, a semântica de referência ocorre automaticamente e a referência "sai do caminho"), existem algumas razões defensáveis ​​pelas quais as referências são modeladas no sistema de tipos em vez de como um atributo separado fora de tipo.

Em primeiro lugar, vamos considerar que nem todos os atributos de um nome declarado devem estar no sistema de tipos. Da linguagem C, temos "classe de armazenamento" e "ligação". Um nome pode ser introduzido como extern const int ri, em que externindica a classe de armazenamento estático e a presença de ligação. O tipo é justo const int.

C ++ obviamente adota a noção de que as expressões têm atributos que estão fora do sistema de tipos. A linguagem agora tem um conceito de "classe de valor", que é uma tentativa de organizar o número crescente de atributos não-tipo que uma expressão pode exibir.

No entanto, as referências são tipos. Por quê?

Costumava ser explicado em tutoriais C ++ que uma declaração como const int &riintroduzida ricomo tendo tipo const int, mas semântica de referência. Essa semântica de referência não era um tipo; era simplesmente uma espécie de atributo que indica uma relação incomum entre o nome e o local de armazenamento. Além disso, o fato de que as referências não são tipos foi usado para racionalizar por que você não pode construir tipos com base em referências, embora a sintaxe de construção de tipo permita. Por exemplo, arrays ou ponteiros para referências não são possíveis: const int &ari[5]e const int &*pri.

Mas, na verdade, as referências são tipos e, portanto, decltype(ri)recupera algum nó de tipo de referência que não está qualificado. Você deve descer além deste nó na árvore de tipos para chegar ao tipo subjacente com remove_reference.

Quando você usa ri, a referência é resolvida de forma transparente, para que ri"tenha a aparência i" e possa ser chamada de "alias" para ela. No sistema de tipos, entretanto, riexiste de fato um tipo que é " referência a const int ".

Por que são tipos de referências?

Considere que se as referências não fossem tipos, essas funções seriam consideradas como tendo o mesmo tipo:

void foo(int);
void foo(int &);

Isso simplesmente não pode ser por razões que são bastante evidentes. Se eles tivessem o mesmo tipo, isso significa que qualquer uma das declarações seria adequada para qualquer uma das definições e, portanto, cada (int)função teria que ser suspeita de receber uma referência.

Da mesma forma, se as referências não fossem tipos, essas duas declarações de classe seriam equivalentes:

class foo {
  int m;
};

class foo {
  int &m;
};

Seria correto que uma unidade de tradução usasse uma declaração e outra unidade de tradução no mesmo programa usasse a outra declaração.

O fato é que uma referência implica uma diferença na implementação e é impossível separar isso do tipo, porque o tipo em C ++ tem a ver com a implementação de uma entidade: seu "layout" em bits, por assim dizer. Se duas funções tiverem o mesmo tipo, elas podem ser chamadas com as mesmas convenções de chamada binária: a ABI é a mesma. Se duas estruturas ou classes têm o mesmo tipo, seu layout é o mesmo, assim como a semântica de acesso a todos os membros. A presença de referências altera esses aspectos dos tipos e, portanto, é uma decisão de design simples incorporá-los ao sistema de tipos. (No entanto, observe um contra-argumento aqui: um membro de estrutura / classe pode ser static, o que também altera a representação; ainda que não seja o tipo!)

Assim, as referências estão no sistema de tipos como "cidadãos de segunda classe" (não muito diferentes de funções e matrizes em ISO C). Existem certas coisas que não podemos "fazer" com referências, como declarar ponteiros para referências ou matrizes deles. Mas isso não significa que não sejam tipos. Eles simplesmente não são tipos de uma forma que faça sentido.

Nem todas essas restrições de segunda classe são essenciais. Dado que existem estruturas de referências, pode haver matrizes de referências! Por exemplo

// fantasy syntax
int x = 0, y = 0;
int &ar[2] = { x, y };

// ar[0] is now an alias for x: could be useful!

Isso simplesmente não é implementado em C ++, só isso. Porém, ponteiros para referências não fazem sentido, porque um ponteiro levantado de uma referência apenas vai para o objeto referenciado. A provável razão pela qual não há matrizes de referências é que as pessoas de C ++ consideram matrizes como um tipo de recurso de baixo nível herdado de C que está quebrado de muitas maneiras que são irreparáveis, e eles não querem mexer em matrizes como o base para qualquer coisa nova. A existência de matrizes de referências, entretanto, seria um exemplo claro de como as referências devem ser tipos.

constTipos não qualificáveis: também encontrados na ISO C90!

Algumas respostas estão sugerindo o fato de que as referências não precisam de um constqualificador. Isso é uma pista falsa, porque a declaração const int &ri = inem mesmo tenta fazer uma constreferência -qualificada: é uma referência a um tipo const-qualificado (que em si não é const). Exatamente como const in *rideclara um ponteiro para algo const, mas esse ponteiro não é const.

Dito isso, é verdade que as constpróprias referências não podem conter o qualificador.

No entanto, isso não é tão bizarro. Mesmo na linguagem ISO C 90, nem todos os tipos podem ser const. Ou seja, as matrizes não podem ser.

Em primeiro lugar, a sintaxe não existe para declarar uma matriz const: int a const [42]está errada.

No entanto, o que a declaração acima está tentando fazer pode ser expresso por meio de um intermediário typedef:

typedef int array_t[42];
const array_t a;

Mas isso não faz o que parece. Nesta declaração, não é o aque é constqualificado, mas os elementos! Ou seja, a[0]é um const int, mas aé apenas "array de int". Consequentemente, isso não requer um diagnóstico:

int *p = a; /* surprise! */

Isso faz:

a[0] = 1;

Novamente, isso reforça a ideia de que as referências são, em certo sentido, "segunda classe" no sistema de tipos, como os arrays.

Observe como a analogia é ainda mais profunda, já que os arrays também têm um "comportamento de conversão invisível", como as referências. Sem que o programador precise usar nenhum operador explícito, o identificador se atransforma automaticamente em um int *ponteiro, como se a expressão &a[0]tivesse sido usada. Isso é análogo a como uma referência ri, quando a usamos como uma expressão primária, denota magicamente o objeto iao qual está ligada. É apenas outro "decaimento" como o "decaimento de array para ponteiro".

E, assim como não devemos ficar confusos com o declínio de "array para ponteiro" em pensar erroneamente que "arrays são apenas ponteiros em C e C ++", também não devemos pensar que as referências são apenas apelidos que não têm nenhum tipo próprio.

Quando decltype(ri)suprime a conversão usual da referência ao seu objeto de referência, isso não é tão diferente de sizeof asuprimir a conversão de array em ponteiro e operar no próprio tipo de array para calcular seu tamanho.

Kaz
fonte
Você tem muitas informações interessantes e úteis aqui (+1), mas é mais ou menos limitado ao fato de que "as referências estão no sistema de tipos" - isso não responde inteiramente à pergunta de OP. Entre esta resposta e a resposta de @ songyuanyao, eu diria que há informações mais do que suficientes para entender a situação, mas parece ser o pano de fundo necessário para uma resposta ao invés de uma resposta completa.
Kyle Strand de
1
Além disso, acho que vale a pena destacar esta frase: "Quando você usa ri, a referência é resolvida de forma transparente ..." Um ponto-chave (que mencionei nos comentários, mas que até agora não apareceu em nenhuma das respostas ) é que decltype não executa essa resolução transparente (não é uma função, portanto, rinão é "usada" no sentido que você descreve). Isso se encaixa muito bem com todo o seu foco no sistema de tipos - a conexão principal é que decltypeé uma operação do sistema de tipos .
Kyle Strand
Hmm, as coisas sobre matrizes parecem uma tangente, mas a frase final é útil.
Kyle Strand
..... embora neste ponto eu já votei a favor de você e não sou o OP, então você não está realmente ganhando ou perdendo nada ao ouvir minhas críticas ....
Kyle Strand
Obviamente, uma resposta fácil (e correta) apenas nos indica o padrão, mas eu gosto do pensamento de fundo aqui, embora alguns possam argumentar que partes dele são suposições. +1.
Steve Kidd
-5

const X & x ”significa x aliases a um objeto X, mas você não pode mudar esse objeto X via x.

E veja std :: is_const .

Sotter.liu
fonte
isso nem mesmo tenta responder à pergunta.
bolov