Почему эти два конструктора вместе не вызывают ошибку неоднозначности? - PullRequest
7 голосов
/ 01 сентября 2011

Рассмотрим следующее:

class A
{
private:
    A() {}
public:
    A(int x = 0) {}
};


int main()
{
    A a(1);
    return 0;
}

У меня есть два конструктора, один по умолчанию, а другой преобразует конструктор с аргументом по умолчанию. Когда я пытался скомпилировать код, я ожидал ошибку неоднозначности, но компилятор не выдает ее.

Даже когда я не создаю экземпляр A, он также не вызывает ошибку неоднозначности.

int main()
{
    return 0;
}

Почему это?

Ответы [ 5 ]

6 голосов
/ 01 сентября 2011

Нет ошибки компиляции, потому что в вашем коде нет ошибки. Точно так же, как определение двух функций: они должны иметь разные подписи, кроме этого, это возможно. Ошибка компиляции неоднозначности появляется только при попытке вызвать одну из этих функций. То же самое произойдет при попытке вызвать конструктор по умолчанию A, даже если он является личным:

class A
{
private:
    A() {}
public:
    A(int x = 0) {}
    void f() {}
    void f(int x = 0) {}
};

Это компилируется, при попытке вызова f() без параметров происходит сбой, что имеет смысл.

Также попробуйте:

class A
{
private:
    A() {}
public:
    A(int x = 0) {}
    void f() {}
    void f() const {}
};

Должно ли это дать ошибку? Нет, потому что два f имеют разные подписи. В этом случае компилятор может устранить неоднозначность, если вы вызовете f для объекта const, будет вызван метод const, и наоборот.

2 голосов
/ 01 сентября 2011

Ваш код компилируется, потому что нет двусмысленности.Вы создали класс с двумя конструкторами, один из которых всегда принимает 0 аргументов, а другой всегда принимает один аргумент, int.Затем вы однозначно вызвали конструктор, принимающий значение типа int.То, что этот int имеет значение по умолчанию, не имеет значения, это все равно совершенно другая подпись.То, что конструкторы потенциально неоднозначны, не имеет значения, компилятор только жалуется, когда конкретный вызов на самом деле неоднозначен.

Когда вы создаете экземпляр A без аргументов, он не знает, какой конструктор вы хотитеcall: конструктор по умолчанию или конструктор, принимающий int со значением параметра 0. В этом случае было бы неплохо, если бы C ++ заметил, что закрытый конструктор неприемлем, но это не всегда возможно.

Это поведение оказывается полезным в некоторых обстоятельствах (например, если у вас есть несколько перегрузок, связанных с шаблонами, некоторые из которых будут перекрываться, если даны правильные типы), хотя для простых случаев, подобных этому, я бы просто сделалодин конструктор с аргументом по умолчанию (желательно помеченный как явный, если у вас нет действительно веской причины оставить его неявным, а затем я бы предпочел эту причину просто для уверенности!)

- EDIT -

Давайте поиграем с этим и попробуем подробнее изучить происходящее.

// A.h
class A
{
public:
    A(); // declare that somewhere out there, there exists a constructor that takes no args.  Note that actually figuring out where this constructor is defined is the linker's job
    A(int x = 10); // declare that somewhere out there, there exists a constructor that take one arg, an integer. Figuring out where it is defined is still the linker's job. (this line does _not_ declare two constructors.)

    int x;
};

// A.cpp
#include "A.h"

A::A() { ... } // OK, provide a definition for A::A()
A::A(int x) { ... } // OK, provide a definition for A::A(int) -- but wait, where is our default param??

// Foo.cpp
#include "A.h"

void Foo()
{
    A a1(24); // perfectly fine
    A a2; // Ambigious!
}

// Bar.cpp
class A // What's going on? We are redefining A?!?!
{
public:
    A();
    A(int x); // this definition doesn't have a default param!
    int x;
};

void Bar()
{
    A a; // This works! The default constructor is called!
}

// Baz.cpp
class A // What's going on? We are redefining A again?!?!
{
public:
    //A(); // no default constructor!
    A(int x = 42); // this definition has a default param again, but wait, it's different!
    int x;
};

void Baz()
{
    A a; // This works! A::A(int) is call! (but with what parameter?)
}

Обратите внимание, что мы пользуемся тем фактом, что компилятор не знает о заголовках;к тому времени, когда он просматривает файл .cpp, препроцессор уже заменил #include телом заголовка.Я играю за то, чтобы быть моим собственным препроцессором, выполняя некоторые опасные вещи, такие как предоставление нескольких разных определений классаПозже одна из задач компоновщика - исключить все эти определения, кроме одного.Если они не выровняются точно правильным образом, произойдут все виды плохих вещей, так как вы окажетесь в сумеречной зоне неопределенного поведения.

Обратите внимание, что я был осторожен, чтобы предоставить точно такой же макет длямой класс в каждой единице компиляции;каждое определение имеет ровно 1 int и 0 виртуальных методов.Обратите внимание, что я не представил никаких дополнительных методов (хотя это может сработать; все же такие вещи следует рассматривать с большим подозрением), единственное, что изменилось, это некоторые не виртуальные функции-члены (на самом деле, конструкторы), а затем толькоудалить конструктор по умолчанию.Изменение и удаление значения по умолчанию ничего не изменило в определении A :: A (int).

У меня нет копии спецификации, поэтому я не могу сказать, выпадают ли мои осторожные измененияпри неопределенном поведении или поведении, специфичном для реализации, но я бы относился к нему как таковому для производственного кода и не использовал бы такие уловки.

И окончательный ответ на то, какой аргумент используется внутри Baz, - .... 42!

1 голос
/ 01 сентября 2011

Объявление потенциально неоднозначных функций в C ++ не приводит к ошибкам двусмысленности. Неоднозначность имеет место, когда вы пытаетесь сослаться на на эти функции неоднозначным образом. В вашем примере двусмысленность произойдет, если вы попытаетесь создать объект по умолчанию.

Строго говоря, совершенно нормально иметь объявления в программе на C ++, которые могут быть неоднозначными (то есть на которые можно ссылаться неоднозначно). Например, на первый взгляд эти перегруженные функции выглядят отлично

void foo(double);
void foo(int);

но вызов foo(1u) вызовет ошибку неоднозначности. Итак, еще раз, неоднозначность - это свойство того, как вы ссылаетесь на ранее объявленные функции, а не свойство самих объявлений функций.

1 голос
/ 01 сентября 2011

Вот слегка измененный пример, который я протестировал с GCC на cygwin:

#include <iostream>

class A
{
  private:
    A();

  public:
    A(int x = 0);
};


A::A()
{
  std::cout << "Constructor 1.\n" << std::endl;
}


A::A(int x)
{
  std::cout << "Constructor 2 with x = " << x << std::endl;
}


int main()
{
  A a1(1);
  A a2;

  return 0;
}

Компилятор выдает следующее сообщение об ошибке:

$ g++ test.cc
test.cc: In function `int main()':
test.cc:28: error: call of overloaded `A()' is ambiguous
test.cc:20: note: candidates are: A::A(int)
test.cc:14: note:                 A::A()

Обновление

Я понимаю, как работает компилятор C ++ (спасибо за объяснения, данные другими): эти два объявления различны и поэтому принимаются, но при попытке обратиться к конструктору по умолчанию компилятор не может решить, какой из двухон должен использовать.

Однако тот факт, что конструктор по умолчанию A () никогда не может быть вызван, может быть выведен из объявлений.Очевидно, у вас есть две функции с разными сигнатурами

A()
A(int x = 0)

, но на самом деле A (int x = 0) неявно определяет две функции: A (int x) и A ().Во второй функции x это просто инициализированная локальная переменная.Вместо записи

A(int x = 0)
{
   ...
}

вы можете написать две функции:

A(int x)
{
  ...
}

A()
{
  int x = 0;
  ...
}

с одинаковым телом.Вторая из этих двух функций имеет ту же сигнатуру, что и конструктор по умолчанию A ().Вот почему у вас всегда будет ошибка компиляции, когда вы пытаетесь создать экземпляр класса A с помощью конструктора A ().Это может быть обнаружено даже без явного объявления, такого как

A a;

Так что я полностью согласен с Ron_s, что я ожидал бы ошибку двусмысленности в его примере.ИМХО было бы более последовательным.

0 голосов
/ 01 сентября 2011

Неопределенность не возникает, потому что закрытый конструктор даже не учитывается, когда вы пишете A a(1), так как вы передаете ему аргумент, а закрытый конструктор не принимает никаких аргументов.

Однако,если вы напишите A a, тогда возникнет неоднозначность, поскольку оба конструктора являются кандидатами, и компилятор не может решить, какой из них вызывать.

...