LINUX.ORG.RU

[C++, CMake, MinGW, кросскомпиляция] try ... catch не ловит исключения.


0

1

Здравствуйте!

Программа собирается с помощью CMake. Одна из целевых ОС - Windows. Реализована следующая фабрика:

Base* load(const char* filename) {
	try {return new Derived1(filename);} catch (...) {}
	try {return new Derived2(filename);} catch (...) {}
	try {return new Derived3(filename);} catch (...) {}
	try {return new Derived4(filename);} catch (...) {}
	try {return new Derived5(filename);} catch (...) {}
	try {return new Derived6(filename);} catch (...) {}
	throw std::runtime_error(std::string("Couldn't load file: ") + filename);
}

При кросскомпиляции с помощью MinGW возникает следующая проблема: исключение, поднятое в конструкторе Derived1 не ловится try...catch. При сборке для linux всё работает. Проблема присутствует при всех вариантах CMAKE_BUILD_TYPE. В качестве дополнительных ключей компилятору задавались только: -Wall -Wextra

Debian Oldstable, софт из оф. репозитория.


Бросать исключения из конструкторов объектов вообще не одобряется. Что делать с недообъектами прикажете? Вообще такая вещь - серьезный повод пересмотреть архитектуру.

cattail ()
Ответ на: комментарий от cattail

Бросать исключения из конструкторов объектов вообще не одобряется.

Ты что курил?

Что делать с недообъектами прикажете?

Разрушать, вызывая деструкторы базовых классов, которые уже отработали.

Begemoth ★★★★★ ()
Ответ на: комментарий от Begemoth

> Ты что курил?

http://www.cyberguru.ru/programming/cpp/cpp-programming-rules2-page73.html

И ещё в паре базовых книг по C++ видел тот же совет.

А если объект во время работы выделил память, а потом уже выбросил исключение, об этом вы подумали?

cattail ()
Ответ на: комментарий от cattail

А если объект во время работы выделил память, а потом уже выбросил исключение, об этом вы подумали?

Конструктор объекта? Да, он за собой должен прибрать. Если память была выделена конструкторами подобъектов, то для этих подобъектов будет вызван деструктор (раздел 15.2 стандарта).

Begemoth ★★★★★ ()
Ответ на: комментарий от Begemoth

Согласен, с каждым пунктом. Но не забывайте, что стандарт до конца не реализован нигде. И это одно из таких мест, где могут быть затыки.

cattail ()
Ответ на: комментарий от cattail

Но не забывайте, что стандарт до конца не реализован нигде

Да, например, export или C++11. Исключения в конструкторах - базовая и важная вещь, если компилятор это не реализует корректно его место на помойке.

Begemoth ★★★★★ ()
Ответ на: комментарий от Begemoth

Заканчивая спор о конструкторах: просмотрел логи непрошедших unit-тестов. Не ловятся все исключения, а не только из конструкторов. При кросскомпиляции тесты запускаются через wine посредством binfmt_misc. Но при их запуске под Windows результат тот же.

SSN ()
Ответ на: комментарий от Reset

> За такой код надо подвесить за яйца, а потом расстрелять.

Не подскажете, что именно не так и как делать правильно?

SSN ()
Ответ на: комментарий от Begemoth

Ну, насчет концептуальной важности для C++, я бы ещё поспорил, если бы не собрал подобный код, и под MinGW он работает. Версия 4.4.3.

Так что вопрос к автору: какая версия MinGW у вас?

cattail ()
Ответ на: комментарий от cattail
$ i586-mingw32msvc-g++ --version
i586-mingw32msvc-g++ (GCC) 4.2.1-sjlj (mingw32-2)
Copyright (C) 2007 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Что ж. Попробую версию посвежее.

SSN ()
Ответ на: комментарий от SSN

Самый простой и тупой метод - завести флаг ошибки в самых Derived, который и проверять на предмет того, корректно ли объект сконструировал себя. Другие оптимизации зависят от того, что в самих Derived происходят.

cattail ()
Ответ на: комментарий от anonymous
/usr/lib/gcc/i586-mingw32msvc/4.2.1-sjlj/../../../../i586-mingw32msvc/bin/ld: cannot find -lgcc_s
SSN ()
Ответ на: комментарий от cattail

> Самый простой и тупой метод - завести флаг ошибки в самых Derived, который и проверять на предмет того, корректно ли объект сконструировал себя.

Иными словами, в случае ошибки получаем сломанный объект с индикатором ошибки в каком-то из его полей. По мне - атомарная операция лучше - либо объект создан и корректен, либо не создан вовсе.

SSN ()
Ответ на: комментарий от SSN

Ну и вынесите тогда его создание в отдельную процедуру, перегрузите оператор new для данного класса, на худой конец.

cattail ()
Ответ на: комментарий от SSN

Потому что таким образом ты потеряешь контроль над программой и не сможешь понять причины феерических глюков, особенно в венде, где catch (...) ловит не только C++ exception'ы

Reset ★★★★★ ()
Ответ на: комментарий от Reset

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

Код:

gee.h:

#ifndef _gee_H_
#define _gee_H_
#ifdef WIN32
#  ifdef gee_EXPORTS
#    define GEE_API __declspec(dllexport)
#  else
#    define GEE_API __declspec(dllimport)
#  endif
#else
#  define GEE_API
#endif

GEE_API void raise();

#endif

gee.cpp:

#include "gee.h"
#include <stdexcept>

void raise() {
	throw std::runtime_error("Hello!");
}

main.cpp:

#include "gee.h"
#include <stdexcept>

int main(int, char**) {
	try {throw std::runtime_error("!!!");} catch (...) {}
	try {raise();} catch (...) {}
	return 0;
}

CMakeLists.txt:

CMAKE_MINIMUM_REQUIRED(VERSION 2.6)

ENABLE_LANGUAGE(CXX)

ADD_LIBRARY(gee SHARED gee.cpp)
ADD_EXECUTABLE(main main.cpp)
ADD_DEPENDENCIES(main gee)
TARGET_LINK_LIBRARIES(main gee)

Toolchain-mingw32.cmake:

# the name of the target operating system
SET(CMAKE_SYSTEM_NAME Windows)

# which compilers to use for C and C++
SET(CMAKE_C_COMPILER i586-mingw32msvc-gcc)
SET(CMAKE_CXX_COMPILER i586-mingw32msvc-g++)
SET(CMAKE_RC_COMPILER i586-mingw32msvc-windres)

# here is the target environment located
SET(CMAKE_FIND_ROOT_PATH
	/usr/i586-mingw32msvc
	~/devel/win32env
)

# adjust the default behaviour of the FIND_XXX() commands:
# search headers and libraries in the target environment, search 
# programs in the host environment
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

Результат запуска программы:

$wine main.exe
terminate called after throwing an instance of 'std::runtime_error'
  what():  Hello!

SSN ()
Ответ на: комментарий от Reset

К этому надо добавить, что это зависит от ключей компилятора, хотя в случае с гцц не уверен.

JackyTreehorn ()
Ответ на: комментарий от JackyTreehorn

Вот про это я не знал.

Visual C++ 8.0 changed the semantics of "catch(...)".
catch(...) no longer catches all SEH exceptions. This is a good thing (see rules 4 and 5).

Но это всё равно не отменяет факта того, что catch(...) — зло. Ловить надо только те exception'ы которые ты ожидаешь, в остальных случаях надо падать.

Reset ★★★★★ ()
Ответ на: комментарий от JackyTreehorn

anonymous выше, похоже был прав. CXXFLAGS = -shared-libgcc должно решить проблему:

# Shared libgcc: If all modules are linked with -shared-libgcc, exceptions can be thrown across DLL boundaries.

# Shared libstdc++: Add -lstdc++_s to your link flags to link against a DLL version of libstdc++.

Правда это всё: New features since the GCC 4.0.0 release. Иду обновляться.

SSN ()
Ответ на: комментарий от cattail

ололо! сконструируй себе объект, который не сконструировался, и при каждом обращении к нему проверя isValid. Флаг тебе в руки!

mi_estas ()
Ответ на: комментарий от cattail

А если объект во время работы выделил память


trololo::trololo
{
 auto_ptr <blablabla> p_m;
 p.reset(new blablabla);

 ...

 if (error) {
    throw std::exception;
 }

 trololo::m = p_m.release();
}

учи матчать. с массивами додумаешь сам.

mi_estas ()

Без исключений я бы делал как-то так:

Base * base =
    Deriver1::create(filename) ||
    Deriver2::create(filename) ||
    ...
    Derived6::create(filename);
    
if ( !base )
    throw std::runtime_error(std::string("Couldn't load file: ") + filename);
    
return base;

Или с помощью фабрик.

Dendy ★★★★★ ()
Ответ на: комментарий от Dendy

> Или с помощью фабрик.

Так это и была фабрика :)

SSN ()
Ответ на: комментарий от Reset

Ловить надо только те exception'ы которые ты ожидаешь, в остальных случаях надо падать.

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

  • Как добиться такого поведения от GCC? Я рецепта не знаю.
  • MSVC, в котором ситуация со спецификацией исключений плоха (по крайней мере была, как в VS2010 - незнаю).
  • Сторонние библиотеки, в которых о спецификации исключений никто не позаботился.
SSN ()
Ответ на: комментарий от Reset

А что, гцц под виндой реализует исключения через SEH? Я думал только MSVC и Intel C++ так делают.

Absurd ★★★ ()
Ответ на: комментарий от SSN

Как добиться такого поведения от GCC? Я рецепта не знаю.

Никак, спецификации исключений проверяются при выполнении программы. В добавок они сильно усложняют шаблонный код. Да и в С++11 их объявили устаревшими (кроме noexcept, который заменяет throw ()).

Begemoth ★★★★★ ()

Алсоу, код конечно ужасен т.к кидать исключения надо только в аварийных ситуциях.

Absurd ★★★ ()

catch (...)

утечки памяти

Ужасно. Я бы сделал так:

#define LEN(x) (sizeof(x)/sizeof(x[0]))
Base* load(const char* filename) {
  typedef Base * (factory_ctr)(const char*);
  factory_ctr * factory_ctrs[] = {
    Deriver1::create,
    Deriver2::create,
    Deriver3::create,
    Deriver4::create,
    Deriver5::create,
    Deriver6::create,
  };
  Base * base = NULL;
  for (int i=0;i<LEN(factory_ctrs);++i)
  {
    base = factory_ctrs[i](filename);
    if (base)
      return base;
  }
  throw std::runtime_error(std::string("Couldn't load file: ") + filename);
}

PS и не слушай того странного парня с «Base * base = одно || другое», такое работает в перле, а не в си++.

legolegs ★★★★★ ()
Ответ на: комментарий от legolegs

и не слушай того странного парня с «Base * base = одно || другое», такое работает в перле, а не в си++.

Только если одно и другое - экземпляры классов или ссылки на них. В случае указателей используется встроенный оператор ||, который вычисляет операнды только при необходимости.

Begemoth ★★★★★ ()
Ответ на: комментарий от Begemoth

Только если одно и другое - экземпляры классов или ссылки на них. В случае указателей используется встроенный оператор ||, который вычисляет операнды только при необходимости.

class Base {};

Base* b() {return new Base();}

void f() {
	Base * base = b() || b();
	return base;
}
~/tmp$ gcc -c blabla.cpp
blabla.cpp: В функции «Base* f()»:
blabla.cpp:7:25: ошибка: cannot convert «bool» to «Base*» in initialization
Status: 1

ЧЯДНТ? :D

geekless ★★ ()
Ответ на: комментарий от legolegs

> Я бы сделал так:

Красиво. Приму к сведению.

SSN ()
Ответ на: комментарий от cattail

А если объект во время работы выделил память, а потом уже выбросил исключение, об этом вы подумали?

Если ты такие вопросы задаёшь, то тебе явно никто кроме капитана помочь не сможет.

mi_estas ()

Исключения из конструкторов кидать нормально, в этом случае объект считается не сконструированным, т.е. не существующим (т.к. конструирование не завершено - значит ничего и нет). Если ты в конструкторе что-то выделил, потом у тебя что-то не получилось и ты кинул исключение - то неправильно, надо сначала всё что выделил освободить обратно, потом кидаться исключениями.

Принцип «убрать за собой, потом кидать исключение» нужен, чтобы реализовать логику «конструирование чего не завершилось, того ещё нет». А чего нет, то не нужно разрушать. Это всё написано в каких-то книжках с красной полоской, в серии которых издаются Александреску, Майерс и т.п. Все эти книжки имеют имена что-то типа «сложные задачи», «новые сложные задачи» и др. Не помню в какой точно.

Понимать что происходит в программе на С++ полезно до уровня памяти и стека, чтобы никакие умные-сложные заимствованные слова, кончающиеся на «тор» не вызывали благоговения, а были умозрительно видимы как конкретные операции над конкретными вещами.

kiverattes ★☆ ()
Ответ на: комментарий от kiverattes

Выше советуют ловить не все exceptions, а только ожидаемые, а на неожидаемые смело падать. Но лучше поймать неизвестное исключение и сказать «ой, неизвестное исключение», сохранить конфиг, записать незаписанное, закрыть незакрытое и потом аккуратно завершЫЦЦа.

kiverattes ★☆ ()
Ответ на: комментарий от kiverattes

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

kiverattes ★☆ ()
Ответ на: комментарий от Absurd

Алсоу, код конечно ужасен т.к кидать исключения надо только в аварийных ситуциях.

В свою защиту:

Бьерн Страуструп. «Язык программирования С++. Специальное издание», параграфы 14.1.1 (Альтернативный взгляд на исключения), 14.5 (Исключения, не являющиеся ошибками):

Механизм обработки исключений ... можно рассматривать в качестве альтернативы механизму возврата из функции. Поэтому вполне законно использовать исключения, которые не имеют никакого отношения к ошибкам. ...можно рассматривать механизмы обработки исключений, как просто ещё одну управляющую структуру.

SSN ()
Ответ на: комментарий от SSN

>Поэтому вполне законно использовать исключения, которые не имеют никакого отношения к ошибкам. ...можно рассматривать механизмы обработки исключений, как просто ещё одну управляющую структуру.

Checked exception из Жабы можно рассматривать как ещё одну управляющую структуру для, скажем так, «ожидаемых неудач». Но их в С++ нет.

Absurd ★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.