¿Por qué no se llama a un método const público cuando el que no es const es 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();
}

El error del compilador es:

error: 'void A :: foo ()' es privado`.

Pero cuando elimino el privado, simplemente funciona. ¿Por qué no se llama al método público const cuando el que no es const es privado?

En otras palabras, ¿por qué la resolución de sobrecarga viene antes que el control de acceso? Esto es extraño. ¿Crees que es consistente? Mi código funciona y luego agrego un método, y mi código de trabajo no se compila en absoluto.

Narek
fuente
3
En C ++, sin un esfuerzo adicional como el uso del idioma PIMPL, no hay una parte "privada" real de la clase. Este es solo uno de los problemas (agregar una sobrecarga de método "privado" y romper el código antiguo de compilación cuenta como un problema en mi libro, incluso si es trivial de evitar simplemente no hacerlo) causado por él.
hyde
¿Existe algún código de la vida real en el que esperaría poder llamar a una función constante, pero que su contraparte no constante sería parte de la interfaz privada? Esto me suena como un mal diseño de interfaz.
Vincent Fourmond

Respuestas:

125

Cuando llama a.foo();, el compilador pasa por la resolución de sobrecarga para encontrar la mejor función para usar. Cuando construye el conjunto de sobrecarga, encuentra

void foo() const

y

void foo()

Ahora, dado aque no lo es const, la versión no constante es la mejor coincidencia, por lo que el compilador elige void foo(). Luego, se establecen las restricciones de acceso y se obtiene un error del compilador, ya que void foo()es privado.

Recuerde, en la resolución de sobrecarga no es "encontrar la mejor función utilizable". Es "encontrar la mejor función e intentar usarla". Si no puede debido a restricciones de acceso o si se está eliminando, aparece un error del compilador.

En otras palabras, ¿por qué la resolución de sobrecarga viene antes que el control de acceso?

Bueno, veamos:

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);
}

Ahora digamos que en realidad no quise hacerlo void foo(Derived * d)privado. Si el control de acceso fuera lo primero, este programa se compilaría, se ejecutaría y Basese imprimiría. Esto podría ser muy difícil de rastrear en una base de código grande. Dado que el control de acceso viene después de la resolución de sobrecarga, obtengo un buen error del compilador que me dice que no se puede llamar a la función que quiero que llame, y puedo encontrar el error mucho más fácilmente.

NathanOliver
fuente
¿Hay alguna razón por la que el control de acceso sea posterior a la resolución de sobrecarga?
drake7707
3
@ drake7707 Como le muestro en mi ejemplo de código, si el control de acceso fuera primero, el código anterior se compilaría, lo que cambia la semántica del programa. No estoy seguro de ti, pero preferiría tener un error y necesitaría hacer una conversión explícita si quisiera que la función permaneciera privada, luego una transmisión implícita y el código "funciona" silenciosamente.
NathanOliver
"y necesito hacer una conversión explícita si quisiera que la función permanezca privada"; parece que el problema real aquí son las conversiones implícitas ... aunque, por otro lado, la idea de que también puede usar una clase derivada implícitamente como la clase base es una característica definitoria del paradigma OO, ¿no es así?
Steven Byks
35

En última instancia, esto se reduce a la afirmación del estándar de que la accesibilidad no debe tenerse en cuenta al realizar la resolución de sobrecargas . Esta afirmación se puede encontrar en [over.match] cláusula 3:

... Cuando la resolución de sobrecarga tiene éxito y la mejor función viable no es accesible (Cláusula [class.access]) en el contexto en el que se utiliza, el programa está mal formado.

y también la Nota en la cláusula 1 de la misma sección:

[Nota: No se garantiza que la función seleccionada por la resolución de sobrecarga sea apropiada para el contexto. Otras restricciones, como la accesibilidad de la función, pueden hacer que su uso en el contexto de llamada esté mal formado. - nota final]

En cuanto a por qué, puedo pensar en un par de posibles motivaciones:

  1. Evita cambios inesperados de comportamiento como resultado de cambiar la accesibilidad de un candidato de sobrecarga (en su lugar, se producirá un error de compilación).
  2. Elimina la dependencia del contexto del proceso de resolución de sobrecarga (es decir, la resolución de sobrecarga tendría el mismo resultado ya sea dentro o fuera de la clase).
Atkins
fuente
32

Suponga que el control de acceso vino antes de la resolución de sobrecarga. Efectivamente, esto significaría que la public/protected/privatevisibilidad controlada en lugar de la accesibilidad.

La sección 2.10 de Diseño y evolución de C ++ de Stroustrup tiene un pasaje sobre esto donde analiza el siguiente ejemplo

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 un beneficio de las reglas actuales (visibilidad antes que accesibilidad) es que (temporalmente) cambiar el private interior class Xen public(por ejemplo, para los propósitos de depuración) es que no hay un cambio tranquilo en el sentido del programa anterior (es decir, X::ase intenta ser accedido en ambos casos, lo que da un error de acceso en el ejemplo anterior). Si public/protected/privatecontrolara la visibilidad, el significado del programa cambiaría (global ase llamaría con private, de lo contrarioX::a ).

Luego afirma que no recuerda si fue por diseño explícito o un efecto secundario de la tecnología de preprocesador utilizada para implementar el predecesor de C con Classess al C ++ estándar.

¿Cómo se relaciona esto con su ejemplo? Básicamente porque la resolución de sobrecarga estándar se ajusta a la regla general de que la búsqueda de nombres viene antes que el control de acceso.

10.2 Búsqueda de nombre de miembro [class.member.lookup]

1 La búsqueda de nombre de miembro determina el significado de un nombre (expresión-id) en un ámbito de clase (3.3.7). La búsqueda de nombres puede resultar en una ambigüedad, en cuyo caso el programa está mal formado. Para una expresión-id, la búsqueda de nombre comienza en el ámbito de la clase de this; para un id-calificado, la búsqueda de nombre comienza en el ámbito del especificador de nombre anidado.La búsqueda de nombres se realiza antes del control de acceso (3.4, Cláusula 11).

8 Si el nombre de una función sobrecargada se encuentra sin ambigüedades, la resolución de sobrecarga (13.3) también tiene lugar antes del control de acceso . Las ambigüedades a menudo se pueden resolver calificando un nombre con su nombre de clase.

TemplateRex
fuente
23

Dado que el thispuntero implícito es non- const, el compilador primero comprobará la presencia de una no constversión de la función antes de una constversión.

Si marca explícitamente el que no es constuno private, la resolución fallará y el compilador no continuará buscando.

Betsabé
fuente
¿Crees que es consistente? Mi código funciona y luego agrego un método y mi código de trabajo no se compila en absoluto.
Narek
Yo creo que sí. La resolución de sobrecarga es intencionalmente delicada. Ayer respondí una pregunta similar: stackoverflow.com/questions/39023325/…
Bathsheba
5
@Narek Creo que funciona igual que las funciones eliminadas en la resolución de sobrecarga. Elige el mejor del conjunto y luego ve que no está disponible, por lo que obtiene un error del compilador. No elige la mejor función utilizable, sino la mejor función y luego intenta usarla.
NathanOliver
3
@Narek También me pregunté primero por qué no funciona, pero considere esto: ¿cómo llamaría a la función privada si la const pública debería elegirse también para objetos no const?
idclev 463035818
20

Es importante tener en cuenta el orden de las cosas que suceden, que es:

  1. Encuentra todas las funciones viables.
  2. Elija la mejor función viable.
  3. Si no hay exactamente una mejor viable, o si realmente no puede llamar a la mejor función viable (debido a violaciones de acceso o la función es deleted), falle.

(3) ocurre después de (2). Lo cual es realmente importante, porque de lo contrario hacer funciones deletedo privatese volvería algo sin sentido y mucho más difícil de razonar.

En este caso:

  1. Las funciones viables son A::foo()y A::foo() const.
  2. La mejor función viable es A::foo()porque la última implica una conversión de calificación en el implícitothis argumento .
  3. Pero lo A::foo()es privatey no tiene acceso a él, por lo tanto, el código está mal formado.
Barry
fuente
1
Uno podría pensar que "viable" incluiría restricciones de acceso relevantes. En otras palabras, no es "viable" llamar a una función privada desde fuera de la clase, ya que no es parte de la interfaz pública de esa clase.
RM
15

Esto se reduce a una decisión de diseño bastante básica en C ++.

Al buscar la función para satisfacer una llamada, el compilador realiza una búsqueda como esta:

  1. Busca hasta encontrar el primer 1 ámbito en el que hay algo con ese nombre.

  2. El compilador encuentra todas las funciones (o functores, etc.) con ese nombre en ese ámbito.

  3. Luego, el compilador sobrecarga la resolución para encontrar el mejor candidato entre los que encontró (sean accesibles o no).

  4. Finalmente, el compilador comprueba si la función elegida es accesible.

Debido a ese orden, sí, es posible que el compilador elija una sobrecarga que no es accesible, aunque hay otra sobrecarga que es accesible (pero no elegida durante la resolución de sobrecarga).

En cuanto a si sería posible hacer las cosas de otra manera: sí, sin duda es posible. Sin embargo, definitivamente conduciría a un lenguaje bastante diferente al de C ++. Resulta que muchas decisiones aparentemente menores pueden tener ramificaciones que afectan mucho más de lo que podría ser inicialmente obvio.


  1. "Primero" puede ser un poco complejo en sí mismo, especialmente cuando / si se involucran plantillas, ya que pueden conducir a una búsqueda en dos fases, lo que significa que hay dos "raíces" completamente separadas desde las que comenzar cuando se realiza la búsqueda. Sin embargo, la idea básica es bastante simple: comience desde el osciloscopio envolvente más pequeño y avance hacia los ámbitos envolventes cada vez más grandes.
Jerry Coffin
fuente
1
Stroustrup especula en D&E que la regla podría ser un efecto secundario del preprocesador utilizado en C con Clases que nunca se revisaron una vez que estuvo disponible más tecnología de compilación avanzada. Mira mi respuesta .
TemplateRex
12

Los controles de acceso ( public, protected, private) no afectan a la sobrecarga de resolución. El compilador elige void foo()porque es la mejor combinación. El hecho de que no sea accesible no cambia eso. Quitarlo solo deja void foo() const, que es entonces la mejor (es decir, la única) coincidencia.

Pete Becker
fuente
11

En esta llamada:

a.foo();

Siempre hay un thispuntero implícito disponible en cada función miembro. Y la constcalificación de thisse toma de la referencia / objeto de llamada. El compilador trata la llamada anterior como:

A::foo(a);

Pero tiene dos declaraciones de las A::foocuales se trata como :

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

Por resolución de sobrecarga, el primero se seleccionará para no constante this, el segundo se seleccionará para a const this. Si quita el primero, el segundo se unirá a ambos consty non-const this.

Después de la resolución de sobrecarga para seleccionar la mejor función viable, viene el control de acceso. Dado que especificó el acceso a la sobrecarga elegida como private, el compilador se quejará.

El estándar lo dice:

[class.access / 4] : ... En el caso de nombres de funciones sobrecargados, el control de acceso se aplica a la función seleccionada por resolución de sobrecarga ....

Pero si haces esto:

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

Entonces, solo constse ajustará la sobrecarga.

WhiZTiM
fuente
Eso es EXTRAÑO que después de la resolución de sobrecarga para seleccionar la mejor función viable, llegue el control de acceso . El control de acceso debe ir antes que la resolución de sobrecarga, ya que si no tuviera acceso, no debería considerarlo en absoluto, ¿qué piensa?
Narek
@Narek, .. He actualizado mi respuesta con una referencia al estándar C ++. De hecho, tiene sentido de esa manera, hay muchas cosas y modismos en C ++ que dependen de este comportamiento
WhiZTiM
9

La razón técnica ha sido respondida por otras respuestas. Solo me enfocaré en esta pregunta:

En otras palabras, ¿por qué la resolución de sobrecarga viene antes que el control de acceso? Esto es extraño. ¿Crees que es consistente? Mi código funciona y luego agrego un método y mi código de trabajo no se compila en absoluto.

Así se diseñó el lenguaje. La intención es intentar llamar a la mejor sobrecarga viable, en la medida de lo posible. Si falla, se activará un error para recordarle que considere el diseño nuevamente.

Por otro lado, suponga que su código se compiló y funcionó bien con la constfunción miembro que se invoca. Algún día, alguien (tal vez usted mismo) decide cambiar la accesibilidad de la constfunción de no miembro de privatea public. Entonces, el comportamiento cambiaría sin errores de compilación. Sería una sorpresa .

Songyuanyao
fuente
8

Porque la variable ade la mainfunción no se declara como const.

Las funciones miembro constantes se llaman en objetos constantes.

Algún tipo programador
fuente
8

Los especificadores de acceso no afectan la búsqueda de nombres y la resolución de llamadas a funciones, nunca. La función se selecciona antes de que el compilador compruebe si la llamada debe desencadenar una infracción de acceso.

De esta forma, si cambia un especificador de acceso, se le avisará en tiempo de compilación si hay una infracción en el código existente; si se tuviera en cuenta la privacidad para la resolución de llamadas a funciones, el comportamiento de su programa podría cambiar silenciosamente.

Kyle Strand
fuente