Почему неявное преобразование не происходит на типизированном nullptr - PullRequest
0 голосов
/ 09 ноября 2019

Я пытался найти способ статически утверждать, что указатель производного класса можно безопасно интерпретировать как указатель на базовый класс, когда я столкнулся с чем-то неожиданным:

Я бы ожидал двух изстатические утверждения в следующем коде дают сбой.

class Base1 {
    int x;
};
class Base2 {
    int y;
};
class Derived : public Base1, public Base2 {
    int z;
};

// one of these should fail???
static_assert( (Derived *)nullptr == (Base1 *)nullptr, "err1" );
static_assert( (Derived *)nullptr == (Base2 *)nullptr, "err2" );
// and one of these as well???
static_assert( (Base1 *)(Derived *)nullptr == nullptr, "err3" );
static_assert( (Base2 *)(Derived *)nullptr == nullptr, "err3" );

Для первой пары утверждений, поскольку ее аргументы имеют тип Derived* и Base1* / Base2*, я ожидал бы Derived* для неявного преобразования в Base1* или Base2* для сравнения. Поскольку Base1 и Base2 не могут занимать одну и ту же память, они не могут быть оба расположены в начале памяти, которую занимает объект Derived, поэтому одно из этих преобразований должно было увеличить значение указателя. В этом случае ненулевой указатель не должен был бы быть равен нулю.

Аналогично, для второй пары я бы ожидал, что явное приведение к Base1* и Base2* должно было изменить одно изуказатели, но они все равно сравниваются равными нулю.

Что здесь происходит?

Ответы [ 2 ]

4 голосов
/ 09 ноября 2019

Нулевой указатель всегда является нулевым указателем и остается нулевым указателем.

[conv.ptr] (выделение мое)

3 Значение типа «указатель на cv D», где D - тип класса, может быть преобразовано в значение типа «указатель на cv B», где B - этобазовый класс D. Если B является недоступным или неоднозначным базовым классом D, программа, которая требует этого преобразования, является плохо сформированной. Результатом преобразования является указатель на подобъект базового класса объекта производного класса. Значение нулевого указателя преобразуется в значение нулевого указателя целевого типа .

Поскольку сравнение Derived* с Base* требует преобразования их в общий тип,это обращение, которое произойдет. И, как вы можете видеть, это сохранение нулевого значения.

Основанием для этого является то, что не должно быть возможности получить действительное значение из значения нулевого указателя. Если вы начали со значения нулевого указателя, вы останетесь с ним.

0 голосов
/ 09 ноября 2019

Введите приведение в стиле C :

Давайте рассмотрим следующую цитату:

Когда встречается выражение преобразования в стиле C, компилятор пытаетсяинтерпретировать его как следующие выражения приведения в следующем порядке: a) const_cast<new_type>(expression) b) static_cast<new_type>(expression)

Если вы попытаетесь использовать reinterpret_cast, , компиляция не удастся потому что reinterpret_cast не является константным выражением, но static_cast делает больше, чем просто приведение для системы типов. В этом случае static_cast, скорее всего, указывает компилятору выполнить некоторое смещение, основанное на (известных во время компиляции) размерах Base1, Base2 и Derived, но static_cast может включать неявные преобразования,вызов конструктора new_type или вызов пользовательского оператора преобразования.

Кроме того, текст из static_cast также гласит следующее:

2)Если new_type является указателем или ссылкой на некоторый класс D, а тип выражения является указателем или ссылкой на его не виртуальную базу B, static_cast выполняет downcast. Это понижение является неправильным, если B является неоднозначным, недоступным или виртуальным основанием (или основанием виртуального основания) из D.

9) Указатель на член некоторого класса D может быть повышен до указателячлену его однозначного, доступного базового класса B. Этот static_cast не проверяет, чтобы убедиться, что член действительно существует в типе времени выполнения указанного объекта.

Это на самом деле потому, что компоновка Derived известен во всей своей полноте, и хотя этот макет технически определяется реализацией, компилятор точно знает этот макет, поскольку ни Base1, ни Base2 не являются недоступными или виртуальными базами.

На самом деле мы видим случайгде это терпит неудачу, предоставляя недоступную базу:

class Base{};

class Base1 : public Base
{
    int x;
};

class Base2 : public Base
{
    int y;
};

class Derived 
    : public Base1
    , public Base2 
{
    int z;
};

constexpr static Derived d{};

constexpr static const Derived* derived_ptr = &d;
constexpr static const Base1* base1_ptr = &d;
constexpr static const Base2* base2_ptr = &d;

// fail due to inaccessible base
static_assert(static_cast<const Derived*>(nullptr) == static_cast<const Base*>(nullptr), "err1" ); // fails
static_assert(static_cast<const Derived*>(derived_ptr) == static_cast<const Base*>(nullptr), "err2" ); // fails

// succeed
static_assert(static_cast<const Derived*>(derived_ptr) == static_cast<const Base1*>(base1_ptr), "err3" );
static_assert(static_cast<const Derived*>(derived_ptr) == static_cast<const Base2*>(base2_ptr), "err4" );
// and one of these as well???
static_assert(static_cast<const Base1*>(static_cast<const Derived*>(derived_ptr)) == base1_ptr, "err5" );
static_assert(static_cast<const Base2*>(static_cast<const Derived*>(derived_ptr)) == base2_ptr, "err6" );
...