Por que um método const público não é chamado quando o método não const é privado?

117

Considere este código:

struct A
{
    void foo() const
    {
        std::cout << "const" << std::endl;
    }

    private:

        void foo()
        {
            std::cout << "non - const" << std::endl;
        }
};

int main()
{
    A a;
    a.foo();
}

O erro do compilador é:

erro: 'void A :: foo ()' é privado`.

Mas quando eu excluo o privado, ele simplesmente funciona. Por que o método const público não é chamado quando o método não const é privado?

Em outras palavras, por que a resolução de sobrecarga vem antes do controle de acesso? Isto é estranho. Você acha que é consistente? Meu código funciona e então adiciono um método, e meu código de trabalho não compila de forma alguma.

Narek
fonte
3
Em C ++, sem esforço extra como o uso do idioma PIMPL, não existe uma parte "privada" real da classe. Este é apenas um dos problemas (adicionar uma sobrecarga de método "privado" e quebrar o código antigo de compilação conta como um problema em meu livro, mesmo que seja trivial evitar por simplesmente não fazê-lo) causado por ele.
hyde de
Existe algum código da vida real em que você esperaria poder chamar uma função const, mas sua contraparte não const faria parte da interface privada? Isso soa como um design de interface ruim para mim.
Vincent Fourmond de

Respostas:

125

Quando você chama a.foo();, o compilador passa pela resolução de sobrecarga para encontrar a melhor função a ser usada. Quando ele constrói o conjunto de sobrecarga, ele encontra

void foo() const

e

void foo()

Agora, como anão é const, a versão não const é a melhor combinação, então o compilador escolhe void foo(). Em seguida, as restrições de acesso são colocadas em prática e você obtém um erro do compilador, pois void foo()é privado.

Lembre-se, na resolução de sobrecarga não é 'encontrar a melhor função utilizável'. É 'encontre a melhor função e tente usá-la'. Se não puder por causa de restrições de acesso ou exclusão, você receberá um erro do compilador.

Em outras palavras, por que a resolução de sobrecarga vem antes do controle de acesso?

Bem, vamos olhar para:

struct Base
{
    void foo() { std::cout << "Base\n"; }
};

struct Derived : Base
{
    void foo() { std::cout << "Derived\n"; }
};

struct Foo
{
    void foo(Base * b) { b->foo(); }
private:
    void foo(Derived * d) { d->foo(); }
};

int main()
{
    Derived d;
    Foo f;
    f.foo(&d);
}

Agora, digamos que eu realmente não pretendesse tornar void foo(Derived * d)privado. Se o controle de acesso viesse primeiro, este programa seria compilado, executado e Baseimpresso. Isso pode ser muito difícil de rastrear em uma grande base de código. Como o controle de acesso vem após a resolução de sobrecarga, recebo um bom erro do compilador informando que a função que desejo que ele chame não pode ser chamada e posso encontrar o bug com muito mais facilidade.

NathanOliver
fonte
Existe alguma razão pela qual o controle de acesso está após a resolução de sobrecarga?
drake7707
3
@ drake7707 Como mostro em meu exemplo de código, se o controle de acesso vier primeiro, o código acima será compilado, o que altera a semântica do programa. Não tenho certeza sobre você, mas prefiro ter um erro e precisar fazer uma conversão explícita se quiser que a função permaneça privada, em seguida, uma conversão implícita e o código "funciona" silenciosamente.
NathanOliver
"e preciso fazer uma conversão explícita se eu quiser que a função permaneça privada" - parece que o verdadeiro problema aqui são as conversões implícitas ... embora, por outro lado, a ideia de que você também pode usar uma classe derivada implicitamente como o classe base é uma característica definidora do paradigma OO, não é?
Steven Byks
35

Em última análise, isso se resume à afirmação na norma de que a acessibilidade não deve ser levada em consideração ao executar a resolução de sobrecarga . Esta afirmação pode ser encontrada na cláusula 3 [over.match] :

... Quando a resolução da sobrecarga é bem-sucedida e a melhor função viável não está acessível (Cláusula [class.access]) no contexto em que é usada, o programa está malformado.

e também a Nota na cláusula 1 da mesma seção:

[Nota: A função selecionada pela resolução de sobrecarga não é garantida como apropriada para o contexto. Outras restrições, como a acessibilidade da função, podem tornar seu uso no contexto de chamada malformado. - nota final]

Quanto ao motivo, posso pensar em algumas motivações possíveis:

  1. Ele evita mudanças inesperadas de comportamento como resultado da alteração da acessibilidade de um candidato a sobrecarga (em vez disso, ocorrerá um erro de compilação).
  2. Ele remove a dependência de contexto do processo de resolução de sobrecarga (ou seja, a resolução de sobrecarga teria o mesmo resultado dentro ou fora da classe).
Atkins
fonte
32

Suponha que o controle de acesso venha antes da resolução de sobrecarga. Efetivamente, isso significaria public/protected/privatevisibilidade controlada em vez de acessibilidade.

A Seção 2.10 de Design e Evolução de C ++ por Stroustrup tem uma passagem onde ele discute o seguinte exemplo

int a; // global a

class X {
private:
    int a; // member X::a
};

class XX : public X {
    void f() { a = 1; } // which a?
};

Stroustrup menciona que um benefício das regras atuais (visibilidade antes da acessibilidade) é que (temporariamente) privateentrar class Xno interior public(por exemplo, para fins de depuração) é que não há nenhuma mudança silenciosa no significado do programa acima (ou seja, X::aé tentado ser acessado em ambos os casos, o que dá um erro de acesso no exemplo acima). Se public/protected/privatecontrolasse a visibilidade, o significado do programa mudaria (global aseria chamado com private, caso contrário X::a).

Ele então afirma que não se lembra se foi por design explícito ou um efeito colateral da tecnologia de pré-processador usada para implementar o C com o predecessor Classess para C ++ Padrão.

Como isso está relacionado ao seu exemplo? Basicamente porque o padrão fez a resolução de sobrecarga estar em conformidade com a regra geral de que a pesquisa de nomes vem antes do controle de acesso.

10.2 Consulta de nome de membro [class.member.lookup]

1 A pesquisa de nome de membro determina o significado de um nome (expressão de id) em um escopo de classe (3.3.7). A pesquisa de nome pode resultar em ambigüidade e, nesse caso, o programa está malformado. Para uma expressão de id, a pesquisa de nome começa no escopo de classe deste; para um id qualificado, a pesquisa de nome começa no escopo do especificador de nestedname. A pesquisa de nome ocorre antes do controle de acesso (3.4, Cláusula 11).

8 Se o nome de uma função sobrecarregada for encontrado sem ambigüidade, a resolução de sobrecarga (13.3) também ocorre antes do controle de acesso . As ambigüidades geralmente podem ser resolvidas qualificando um nome com seu nome de classe.

TemplateRex
fonte
23

Visto que o thisponteiro implícito é não- const, o compilador primeiro verificará a presença de uma não- constversão da função antes de uma constversão.

Se você marcar explicitamente o não- constum private, a resolução falhará e o compilador não continuará pesquisando.

Bate-Seba
fonte
Você acha que é consistente? Meu código funciona e, em seguida, adiciono um método e meu código de trabalho não compila.
Narek
Eu acho que sim. A resolução de sobrecarga é intencionalmente complicada. Eu respondi a uma pergunta semelhante ontem: stackoverflow.com/questions/39023325/…
Bathsheba
5
@Narek Eu acredito que funciona exatamente como as funções excluídas na resolução de sobrecarga. Ele escolhe o melhor do conjunto e vê que não está disponível, então você obtém um erro do compilador. Ele não escolhe a melhor função utilizável, mas a melhor função e então tenta usá-la.
NathanOliver
3
@Narek Eu também me perguntei primeiro por que isso não funciona, mas considere o seguinte: como você chamaria a função privada se a const one pública devesse ser escolhida também para objetos não const?
idclev 463035818
20

É importante ter em mente a ordem das coisas que acontecem, que é:

  1. Encontre todas as funções viáveis.
  2. Escolha a melhor função viável.
  3. Se não houver exatamente um melhor viável, ou se você não puder realmente chamar a melhor função viável (devido a violações de acesso ou a função sendo deleted), falhe.

(3) acontece depois de (2). O que é muito importante, porque, de outra forma, criar funções deleted ou privateficaria meio sem sentido e muito mais difícil de raciocinar.

Nesse caso:

  1. As funções viáveis ​​são A::foo()e A::foo() const.
  2. A melhor função viável é A::foo()porque a última envolve uma conversão de qualificação no thisargumento implícito .
  3. Mas A::foo()é privatee você não tem acesso a ele, portanto, o código está mal formado.
Barry
fonte
1
Alguém poderia pensar que "viável" incluiria restrições de acesso relevantes. Em outras palavras, não é "viável" chamar uma função privada de fora da classe, pois não faz parte da interface pública dessa classe.
RM de
15

Isso se resume a uma decisão de design bastante básica em C ++.

Ao pesquisar a função para satisfazer uma chamada, o compilador realiza uma pesquisa como esta:

  1. Ele pesquisa para encontrar o primeiro 1 escopo no qual há algo com esse nome.

  2. O compilador encontra todas as funções (ou functores, etc.) com aquele nome naquele escopo.

  3. Em seguida, o compilador sobrecarrega a resolução para encontrar o melhor candidato entre aqueles que encontrou (sejam eles acessíveis ou não).

  4. Finalmente, o compilador verifica se a função escolhida está acessível.

Por causa dessa ordem, sim, é possível que o compilador escolha uma sobrecarga que não está acessível, embora haja outra sobrecarga que está acessível (mas não escolhida durante a resolução da sobrecarga).

Sobre se seria possível fazer diferente: sim, sem dúvida é possível. Isso definitivamente levaria a uma linguagem bastante diferente do C ++. Acontece que muitas decisões aparentemente menores podem ter ramificações que afetam muito mais do que seria inicialmente óbvio.


  1. "Primeiro" pode ser um pouco complexo em si, especialmente quando / se os modelos são envolvidos, uma vez que podem levar a uma pesquisa em duas fases, o que significa que há duas "raízes" totalmente separadas para começar ao fazer a pesquisa. A ideia básica é bem simples: comece com o menor escopo envolvente e vá abrindo caminho para os escopos cada vez maiores.
Jerry Coffin
fonte
1
Stroustrup especula em D&E que a regra pode ser um efeito colateral do pré-processador usado em C com classes que nunca foram revisadas quando mais uma tecnologia avançada de compilador tornou-se disponível. Veja minha resposta .
TemplateRex
12

Controles de acesso ( public, protected, private) não afetam sobrecarga resolução. O compilador escolhe void foo()porque é a melhor combinação. O fato de não ser acessível não muda isso. Removê-lo deixa apenas void foo() const, que é a melhor (ou seja, única) correspondência.

Pete Becker
fonte
11

Nesta chamada:

a.foo();

Sempre há um thisponteiro implícito disponível em cada função de membro. E a constqualificação de thisé retirada da referência / objeto de chamada. A chamada acima é tratada pelo compilador como:

A::foo(a);

Mas você tem duas declarações das A::fooquais são tratadas como :

A::foo(A* );
A::foo(A const* );

Por resolução de sobrecarga, o primeiro será selecionado para não const this, o segundo será selecionado para a const this. Se você remover o primeiro, o segundo se ligará a ambos conste non-const this.

Após a resolução da sobrecarga para selecionar a melhor função viável, vem o controle de acesso. Como você especificou o acesso à sobrecarga escolhida como private, o compilador irá reclamar.

O padrão diz isso:

[class.access / 4] : ... No caso de nomes de função sobrecarregados, o controle de acesso é aplicado à função selecionada pela resolução de sobrecarga ....

Mas se você fizer isso:

A a;
const A& ac = a;
ac.foo();

Então, apenas a constsobrecarga será adequada.

WhiZTiM
fonte
Isso é ESTRANHO que após a resolução da sobrecarga para selecionar a melhor função viável, vem o controle de acesso . O controle de acesso deve vir antes da resolução de sobrecarga, como se você não tivesse acesso, caso não devesse considerá-lo, o que você acha?
Narek
@Narek, .. Atualizei minha resposta com uma referência ao padrão C ++. Na verdade, faz sentido assim, há muitas coisas e expressões idiomáticas em C ++ que dependem desse comportamento
WhiZTiM
9

A razão técnica foi respondida por outras respostas. Vou me concentrar apenas nesta questão:

Em outras palavras, por que a resolução de sobrecarga vem antes do controle de acesso? Isto é estranho. Você acha que é consistente? Meu código funciona e, em seguida, adiciono um método e meu código de trabalho não compila.

É assim que a linguagem foi projetada. A intenção é tentar chamar a melhor sobrecarga viável, na medida do possível. Se falhar, um erro será acionado para lembrá-lo de considerar o projeto novamente.

Por outro lado, suponha que seu código compilado e constfuncione bem com a função de membro que está sendo chamada. Algum dia, alguém (talvez você) decide alterar a acessibilidade da constfunção de não membro de privatepara public. Então, o comportamento mudaria sem erros de compilação! Isso seria uma surpresa .

Songyuanyao
fonte
8

Porque a variável ana mainfunção não é declarada como const.

Funções-membro constantes são chamadas em objetos constantes.

Algum cara programador
fonte
8

Os especificadores de acesso não afetam a pesquisa de nome e a resolução de chamada de função, nunca. A função é selecionada antes que o compilador verifique se a chamada deve acionar uma violação de acesso.

Dessa forma, se você alterar um especificador de acesso, será alertado em tempo de compilação se houver uma violação no código existente; se a privacidade for levada em consideração para a resolução da chamada de função, o comportamento do seu programa pode mudar silenciosamente.

Kyle Strand
fonte