3

Собственно, ковыряю то, что написано в теме вопроса. Проблема возникла, когда я намеренно написал косячный код, а он, блин не упал.

Собственно, код:

#include <iostream>
#include <memory>

struct cdmem {
    cdmem()             { std::cout << this << " :: cdmem::cdmem()"             << std::endl; };
    cdmem(const cdmem&) { std::cout << this << " :: cdmem::cdmem(const cdmem&)" << std::endl; };
    cdmem(const cdmem*) { std::cout << this << " :: cdmem::cdmem(const cdmem*)" << std::endl; };

    void testf()        { std::cout << this << " :: testf()"                    << std::endl; };

    ~cdmem()            { std::cout << this << " :: cdmem::~cdmem()"            << std::endl; };
};


int main() {
    std::unique_ptr<cdmem> pd(new cdmem);
    if(!pd) {
        std::cout << "\n ERR1 " << std::endl;
        return 1;
    }
    pd->testf();
    auto pd1(std::move(pd));
    pd->testf(); // Вот тут указатель уже нулевой
    return 0;
}

Вывод:

0x801c06058 :: cdmem::cdmem()
0x801c06058 :: testf()
0x0 :: testf() // this равен 0x0
0x801c06058 :: cdmem::~cdmem()

Получается, что обращения к самому объекту не происходит?

ixSci
  • 23,825
Nex
  • 345
  • 4
    Ну, разыменование нулевого указателя — это UB, никто ничего не гарантирует, в том числе и падения. – VladD Sep 11 '15 at 18:54

4 Answers4

4

Это классическое UB, (обращение к более невалидному указателю), которое в данном случае выражается в нормальной работе. Это частный случай, который в другой ситуации (навскидку - виртуальный метод и вырубленная оптимизация) выстрелит вам в ногу. Не делайте так.

gbg
  • 22,253
  • 1
    Не могли бы вы предоставить ссылку из Стандарта, подтверждающую, что такой вызов функции ведет к неопределенному поведению. – Vlad from Moscow Sep 11 '15 at 19:34
  • Я могу разве что отправить http://habrahabr.ru/company/pvs-studio/blog/250701 в блог PVS Studio на Хабре, где есть пара огромных статей с разъяснениями UB при заигрываниях с невалидными указателями. Разыменовывать невалидный указатель точно запрещено. Оператор "стрелка", по сути, разыменовывает указатель. Вывод - UB. – gbg Sep 11 '15 at 19:39
  • Меня статьи не интересуют. Статьи - это не стандарт. Это всего лишь частная точка зрения на стандарт. Меня интересует именно этот конкретный случай с точки зрения стандарта. – Vlad from Moscow Sep 11 '15 at 19:42
  • 1
    Та статья является как раз результатом глубокой проработки Стандарта (в ней есть ссылки на нужные пункты и переводы) и общения с экспертами. – gbg Sep 11 '15 at 19:45
  • 1
    @VladfromMoscow: В последнем драфте (N4527), 20.8.1.2.4/3 ([unique.ptr.single.observers]): pointer operator->() const noexcept; requires: get() != nullptr. Ссылка на драфт здесь. – VladD Sep 11 '15 at 22:03
  • @VladD Это требование относится к классу std::unique_ptr. А меня интересует вообще конструкция ptr->func(), где ptr - это nullptr, когда внутри функции нет обращения к членам класса, как в данном вопросе. – Vlad from Moscow Sep 11 '15 at 22:16
  • @VladfromMoscow: Ну, приведённый текст уже гарантирует UB, без ссылок на разыменование nullptr. А по поводу разыменования nullptr сейчас поищу. – VladD Sep 11 '15 at 22:20
  • @VladD Проблема в том, что в C выражение ptr, где ptr - это null указатель, является, похоже, корректным, так как там написано, что "If an invalid value has been assigned to the pointer, the behavior of the unary operator is undefined.102)", но NULL указатель является валидным значением,, о чем говорится в другом месте. – Vlad from Moscow Sep 11 '15 at 22:25
  • @VladfromMoscow: Я не сообразил сам, но мне кажется убедительным это объяснение. – VladD Sep 11 '15 at 23:25
3

Тут играет роль неопределённое поведение.

#include <iostream>

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

int main()
{
    A * a = nullptr;
    a->foo();
}

Вывод:

A::foo()

Это работает в MinGW 5.1.0, но далеко не факт что это будет работать в других версиях и в других компиляторах.

  • 1
    А также с этим компилятором с другими опциями компиляции. А также с этим компилятором, если вы вызовете эту функцию ещё откуда-то. А также с этим компилятором, если вы немного измените код, и оптимизатор соптимизирует чуть-чуть по-другому. – VladD Sep 11 '15 at 19:00
  • Даже оптимизация -O0 (компилировал clang++36) не уронила код в исходном виде. Падать начало только когда добавил дополнительный член (int) в структуру с явным обращением к нему. – Nex Sep 11 '15 at 19:11
  • @VladD, теоретически - да. Но мне не удавалось воспроизвести такую ситуацию. Видимо это особенность GCC. – Антон Сазонов Sep 11 '15 at 19:13
  • @Nex, это логично, ведь его нет по адресу 0. – Антон Сазонов Sep 11 '15 at 19:14
  • 1
    @АнтонСазонов: Это скорее особенности слишком простого кода. Для более сложного кода оптимизатор может найти другую возможность для оптимизации. Оптимизаторы становятся всё умнее и умнее, и активно используют UB для упрощения кода. Вот классическая статья по этому поводу: http://blog.regehr.org/archives/213. И ещё: http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html – VladD Sep 11 '15 at 19:15
  • @VladD, возможно. Спасибо за статью. – Антон Сазонов Sep 11 '15 at 19:18
  • @VladD Спасибо за статьи. – Nex Sep 11 '15 at 19:21
  • Пожалуйста! UB, как оказывается, гораздо более непредсказуемая вещь, чем на первый взгляд. – VladD Sep 11 '15 at 19:22
  • На самом деле, я думаю тут не всё так сложно, но обсуждать это здесь явно не стоит. – Антон Сазонов Sep 11 '15 at 19:27
  • @VladD, просто тут нечего оптимизировать. Если отбросить все крестовые заморочки, то в сухом остатке мы получаем функцию _ZN5cdmem5testfEv, первым аргументом которой является this (а при ее вызове это pd). Поскольку функция по этому адресу (и смещениям от него) не обращается, то "падения" не возникает. / Вот и все. – avp Sep 11 '15 at 21:45
  • @avp: Если вы почитаете указанную статью, то увидите ещё один возможный путь: оптимизатор может доказать, что pd в точке разыменования есть null при всех возможных путях выполнения main, поэтому при выполнении гарантированно возникает UB, а значит, можно заменить main на return 0;. – VladD Sep 11 '15 at 21:50
  • @VladD, ну, какой-то компайлер может и пойдет по этому пути, а g++ -O3 все же вызывает testf с обнуленным аргументом (может в GNU думают, что программист хочет исследовать UB? :)) – avp Sep 11 '15 at 22:06
  • @avp: Ну, оптимизаторы становятся всё более и более агрессивными. Не зря же, по слухам, ядро не компилируют с -O3. – VladD Sep 11 '15 at 22:07
  • 1
    @VladD, насколько мне известно -- да, ядро компилируют с -O2 (озон намного опасней кислорода :)) – avp Sep 11 '15 at 22:09
  • @avp: На самом деле мне кажется это серьёзной проблемой. Компиляторы стали намного умнее людей, что приводит к проблемам в безопасности. Возможно, нужен специальный, более безопасный диалект C для ядра и компонент, отвечающих за security. – VladD Sep 11 '15 at 22:16
1

Классический случай UB. Если хотите чтобы 100% программа упала, попробуйте добавить в исследуемую структуру любое поле с данными (например int field = 12;) и обратиться к нему в вызываемой функции.

aleks.andr
  • 2,489
0

Вызов невиртуальной функции компилируется в обычный вызов функции с передачей параметром указателя this, в данном случае NULL. Если вы хотите настоящий UB - вызывайте виртуальную функцию.