Es porque la búsqueda de nombres se detiene si encuentra un nombre en una de sus bases. No mirará más allá de otras bases. La función en B sombrea la función en A. Tienes que volver a declarar la función de A en el alcance de B, de modo que ambas funciones sean visibles desde B y C:
class A
{
public:
void foo(string s){};
};
class B : public A
{
public:
int foo(int i){};
using A::foo;
};
class C : public B
{
public:
void bar()
{
string s;
foo(s);
}
};
Editar: La descripción real que da el Estándar es (de 10.2 / 2):
Los siguientes pasos definen el resultado de la búsqueda de nombres en el ámbito de una clase, C. Primero, se considera cada declaración del nombre en la clase y en cada uno de sus subobjetos de clase base. Un nombre de miembro f en un subobjeto B oculta un nombre de miembro f en un subobjeto A si A es un subobjeto de clase base de B. Cualquier declaración que esté tan oculta se elimina de consideración. Cada una de estas declaraciones que fue introducida por una declaración-using se considera que es de cada subobjeto de C que es del tipo que contiene la declaración designada por la declaración-using.96) Si el conjunto resultante de declaraciones no es todos de subobjetos del mismo tipo, o el conjunto tiene un miembro no estático e incluye miembros de distintos subobjetos, existe una ambigüedad y el programa está mal formado. De lo contrario, ese conjunto es el resultado de la búsqueda.
Tiene lo siguiente que decir en otro lugar (justo encima):
Para una expresión-id [ algo como "foo" ], la búsqueda de nombre comienza en el ámbito de la clase de this; para un id-calificado [ algo así como "A :: foo", A es un especificador-nombre-anidado ], la búsqueda de nombre comienza en el alcance del especificador-nombre-anidado. La búsqueda de nombres se realiza antes del control de acceso (3.4, cláusula 11).
([...] puesto por mí). Tenga en cuenta que eso significa que incluso si su foo en B es privado, el foo en A todavía no se encontrará (porque el control de acceso ocurre más tarde).