вторник, 6 июля 2010 г.

[prog.flame] В продолжение вчерашней темы – мой ответ экспертам board.rt.mipt.ru

Моя вчерашняя заметка “Вот за что мне не нравится C++” засветилась на форуме board.rt.mipt.ru. Как и следовало ожидать, тамошними экспертами я был объявлен тупым и криворуким.

Понятное дело, слышать, что у тебя “такой маленький” неприятно даже в том случае, если это далеко не так. Так что запасаемся линейками и приступаем :)

Для начала я объясню со слайдами, в чем была проблема у Стива Хьюстона с классом ACE_INET_Addr, поскольку один из высказавшихся экспертов, очевидно, не понял о чем речь. В классе ACE_INET_Addr имеется такой код (из ACE 5.6.8):

/**
* @class ACE_INET_Addr
*
* @brief Defines a C++ wrapper facade for the Internet domain address
* family format.
 */
class ACE_Export ACE_INET_Addr : public ACE_Addr
{
  ...
private:
  /// Underlying representation.
  /// This union uses the knowledge that the two structures share the
  /// first member, sa_family (as all sockaddr structures do).
  union
  {
    sockaddr_in  in4_;
#if defined (ACE_HAS_IPV6)
    sockaddr_in6 in6_;
#endif /* ACE_HAS_IPV6 */
  } inet_addr_;
};

Обратить внимание следует на поле inet_addr_, которое является объединением. Если при компиляции определен символ ACE_HAS_IPV6, то объединение включает и sockaddr_in, и sockaddr_in6, а размер inet_addr_ определяется размером sockaddr_in6 (т.к. эта структура больше). Если же ACE_HAS_IPV6 при компиляции не определен, то inet_addr_ содержит только sockaddr_in.

Так вот проблема Стива Хьюстона была в том, что сама библиотека ACE была скомпилирована с ACE_HAS_IPV6, т.к. ACE предполагала, что sizeof(inet_addr_) == sizeof(sockaddr_in6). Однако, прикладная программа была скомпилирована без ACE_HAS_IPV6. Соответственно, в программе sizeof(inet_addr_) == sizeof(sockaddr_in). Т.е. в программе под объект ACE_INET_Addr на стеке отводилось меньше места, чем на это расчитывала библиотека ACE. Поэтому происходило следующее: в программе на стеке отводилось место (23 байта под inet_addr_), далее запускался конструктор ACE_INET_Addr (запускался код из ACE), конструктор ACE_INET_Addr обнулял содержимое inet_addr_ – но не 23 байта, а 28 байт! Отсюда и грабли, на поиск которых было потрачено 5 часов рабочего времени.

А теперь парочка примеров того, как можно отгрести на ровном месте проблемы с C++ кодом, если чуть ошибиться с опциями компиляции для разных файлов. Ответов я давать не буду, пусть желающие попробуют предсказать результат с ходу. Примеры проверялись на Visual C++ 2003 и Visual C++ 2008. Архив с примерами находится здесь. Для компиляции достаточно зайти в нужный подкаталог и запустить в нем nmake.

Исходные тексты примеров так же можно увидеть под катом. Но сначала я хочу сказать, при чем здесь язык C++, ведь упомянутые ошибки не относятся напрямую к языку C++. Да, это не проблемы языка C++, это проблемы инструмента под названием C++. А вот как раз этот инструмент оказывается очень хрупким и чувствительным к настройкам компилятора и линкера. Вполне может быть, что аналогичными проблемами страдают и другие инструменты (тот же C, может быть Ada и, вполне возможно, Eiffel, т.к. он компилируется через C-шное представление). Но меня волнуют именно проблемы C++.

Первый пример:

  • файл t1_1.hpp
    #if !defined( T1_1_HPP )
    #define T1_1_HPP

    class base_t
       {
       public :
          virtual ~base_t() {}

          virtual const char *
          name() const = 0;
       };

    class d1_t : public base_t
       {
       public :
          virtual const char *
          name() const { return "d1"; }
       };

    class d2_t : public base_t
       {
       public :
          virtual const char *
          name() const { return "d2"; }
       };

    #endif
  • файл t1_1.cpp
    #include "t1_1.hpp"

    const char *
    f( const base_t & b )
       {
          const d1_t * d1 = dynamic_cast< const d1_t * >(&b);
          if( d1 )
             return "d1 detected";

          const d2_t * d2 = dynamic_cast< const d2_t * >(&b);
          if( d2 )
             return "d2 detected";

          return "nothing detected";
       }
  • файл t2_main.cpp
    #include <cstdio>

    #include "t1_1.hpp"

    const char * f( const base_t & b );

    int
    main()
       {
          d1_t d;

          const char * result = f( d );

          std::printf( "result: %s\n", result );
       }
  • файл Makefile
    t1.exe: t1_1.obj t1_main.obj
       link /SUBSYSTEM:CONSOLE /OUT:t1.exe  t1_1.obj t1_main.obj

    t1_1.obj:
       cl -c -GR t1_1.cpp

    t1_main.obj:
       cl -c -GR- t1_main.cpp

Второй пример:

  • файл t2_1.hpp
    #if !defined( T2_1_HPP )
    #define T2_1_HPP

    #include <cstdio>

    class some_raii_t
       {
       public :
          some_raii_t() { std::printf( "resource acquisition\n" ); }
          ~some_raii_t() { std::printf( "resource freeing\n" ); }
       };

    #endif
  • файл t2_a.cpp
    #include "t2_1.hpp"

    void b();

    void a()
       {
          some_raii_t raii;

          b();
       }
  • файл t2_b.cpp
    #include <stdexcept>

    void b()
       {
          throw std::runtime_error( "some exception" );
       }
  • файл t2_main.cpp
    #include <cstdio>
    #include <exception>

    void a();

    int
    main()
       {
          try
             {
                a();
                std::printf( "no exceptions\n" );
             }
          catch( const std::exception & x )
             {
                std::printf( "exception caught: %s\n", x.what() );
             }
       }
  • файл Makefile
    t1.exe: t2_a.obj t2_b.obj t2_main.obj
       link /SUBSYSTEM:CONSOLE /OUT:t2.exe t2_a.obj t2_b.obj t2_main.obj

    t2_a.obj:
       cl -c -GR t2_a.cpp

    t2_b.obj:
       cl -c -GR -EHs t2_b.cpp

    t2_main.obj:
       cl -c -EHs t2_main.cpp

26 комментариев:

  1. Проспойлю... Dynamic_cast<> does some runtime type checking and the /GR- compiler flag
    (which is the default) disables RTTI.

    Вообще наплевать на непониматоров; проблемы надо обсуждать с теми, кто возможно их будет решать.

    Хорошо бы обсудить "как должна *была бы* выглядеть в с++ поддержка умолчаний" (сюда входят как define, так и параметры по умолчанию в шаблонах, которые жцц разворачивает в сообщениях об ошибках). К "умолчаниям" еще надо добавить "соглашения", но это возможно более сложный вопрос.

    ОтветитьУдалить
  2. > board.rt.mipt.ru.

    Зашел, почитал заголовки комментариев... Ой, Женя, ты туда больше не ходи :-), там кроме школьников (причем второгодников) никого нет. Лучше на ЛОР ходи. Там позавчера один анонимус с другим спорили про Ruby (новость, разумеется, про Python была). Причем очень аргументированно и содержательно спорили. Так забавно. Сначала один анонимус пишет, а затем другой ему отвечает (может их и больше было, кто их разберет...) и большей частью по делу.

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

    как мы гарантируем неперерасход памяти, если:

    1. лицензия состоит из очень длинных строк в перемешку с непустыми очень короткими

    2. в файле встречаются как очень длинные, так и очень короткие строки

    Если скользящее окошко будет всегда размером в к-во строк лицензии, то размер памяти под его данные может быть в M раз больше к-ва памяти лицензии, где М может быть даже порядка длина_лицензии_в_байтах/10

    ОтветитьУдалить
  4. Этот комментарий был удален автором.

    ОтветитьУдалить
  5. допустим, в лицензии 100 строк по 40 символов и 5 строк по 10000 символов;

    в файле все строки либо по 40 символов, либо по 10000 символов;

    в случае если в файле подряд идут 105 строк по 10000 символов расход памяти будет?

    если же предполагается какй-то более умный способ наполнения скользящего окошка, то его неплохо бы описать словами; у него, вероятно, снова будут недостатки.

    ОтветитьУдалить
  6. пока blogstop тормозит, напишу еще чуть-чуть (и пойду гулять):

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

    Рядом проблема -- это persistence объектов таких вот классов, *тоже* с разными опциями, а иногда и с одинаковыми!

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

    ОтветитьУдалить
  7. 2имя:

    По поводу C++. Я думаю, что C++ как раз велик тем, что его можно адаптировать к разным нишам. Где-то не нужны RTTI, где-то исключения. За счет этого он и держится.

    Но, имхо, следовало бы сделать две вещи:

    1. Уже давно пора было бы веделить некоторое подмножество C++ в более безопасный язык (хоть бы даже и под названием Enterprise С++). В котором нельзя было бы отключать RTTI, исключения и пр. стандартные возможности. В котором для примитивных типов данных были бы зафиксированы строгие размерности (скажем, int всегда 32-бита и все!). Может быть были бы запрещены некоторые преобразования указателей. Может быть, был бы урезан препроцессор.

    2. Производителям компиляторов следовало бы сохранять в объектных файлах/библиотеках параметры компиляции. А линкеру следовало бы проверять опции разных объектников/библиотек на совместимость.

    ОтветитьУдалить
  8. 2имя:

    По поводу тестовой задачи. Да, в сценарии со строками разной длины перерасход памяти наверняка будет. Но для тестовой задачи, на реализацию и тестирование которой отводится ограниченное время -- это вполне допустимо.

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

    ОтветитьУдалить
  9. 2san:

    Я по статистике посещаемости увидел, что толпа народу оттуда ко мне ломится, вот и интересно стало, что и почему.

    >Лучше на ЛОР ходи.

    Новости на главной я там регулярно читаю. Ходить по форумам и активно участвовать уже не могу, не хватает ни времени, ни сил.

    А вообще, по количеству интересных и грамотных людей LOL не сильно RSDN-у уступает (если вообще уступает).

    ОтветитьУдалить
  10. ЕО>1. Уже давно пора было бы веделить некоторое подмножество C++ в более безопасный язык (хоть бы даже и под названием Enterprise С++). В котором нельзя было бы отключать RTTI, исключения и пр. стандартные возможности. В котором для примитивных типов данных были бы зафиксированы строгие размерности (скажем, int всегда 32-бита и все!). Может быть были бы запрещены некоторые преобразования указателей. Может быть, был бы урезан препроцессор.

    Objective C?

    ЕО> А вообще, по количеству интересных и грамотных людей LOL не сильно RSDN-у уступает (если вообще уступает).

    а куда там заходить?
    какого-то специализированного форума по плюсам не нашел, а все перечитывать никакого времени не хватит.

    ОтветитьУдалить
  11. >Objective C?

    Несколько раз пытался смотреть на него. Так и не понял, как этой смесью C и SmallTalk можно пользоваться.

    Меня бы вполне устроил Eiffel. Но это экономически не оправдано в СНГ. Да и библиотек для него вообще нет.

    >а куда там заходить?

    http://www.linux.org.ru/forum/development/ -- там обычно в названии темы указывают язык программирования.

    ОтветитьУдалить
  12. Уже давно пора было бы веделить некоторое подмножество C++ в более безопасный язык

    Притом прототип для этого дела есть http://www.digitalmars.com/d/2.0/safed.html можно очень многое оттуда позаимствовать.

    ОтветитьУдалить
  13. >Притом прототип для этого дела есть http://www.digitalmars.com/d/2.0/safed.html можно очень многое оттуда позаимствовать.

    Угу. По большому счету, и сам D мог бы стать таким языком. Если бы у него автор был более прагматичным.

    ОтветитьУдалить
  14. 2night beast: вот, кстати, еще о ЛОР-е. На RSDN еще не было новости, а на ЛОР-е обсуждают выход новой спецификации языка Haskell -- http://www.linux.org.ru/news/doc/5083611

    ОтветитьУдалить
  15. По поводу тестовой задачи. Да, в сценарии со строками разной длины перерасход памяти наверняка будет.

    А в итераторном решении -- не будет.

    Впрочем, окошко еще видимо можно спасти: делаем класс MyString, который не храниm0; данные тех строк, что длиннее самой длинной в лицензии (только длинну), и держим в окошке строк общей длинной <= длины_лицензии.

    Монстровато и нереюзабельный велосипед, но реализуемо.

    Итераторное решение состоит из 2 классов, первый из которых можно найти кажется в буст::спирит (но я его накатал сам в 30 строк):

    /// главное -- итератор можно копировать, и он помнит позицию, и продолжает итерироваться оттуда;
    /// итератор не инвалидируется при fseek(file), и сам вызывает fseek (при конструировании и изредка при инкременте);
    /// два итератора равны при равенстве позиций от начала файла, равенство файлов не проверяется
    /// после чтения с диска итератор позволяет сделать allowed быстрых инкрементов до следующего чтения с диска;
    /// итератор позволяет сделать allowed быстрых декрементов, но только после соответствующего числа инкрементов;
    /// декремент введен как (опасная) и быстрая альтернатива копированию итератора и инкременту копии;
    ///
    class FileIterator: public std::iterator_traits

    /// Iterator должен быть либо итератором двунаправленного доступа, либо FileIterator
    template class NormalizedCharIterator: public std::iterator_traits

    выхлоп ненормализованного файла делаем так: у NormalizedCharIterator имеется метод origin(), который возвращет "настоящий" итератор; файл через итератор прогоняется до тех пор, пока окажется не равен origin-у найденной лицензии

    Нереюзабельного велосипедостроительства -- ноль.

    ОтветитьУдалить
  16. блин, вырезало "тэги"

    class FileIterator: public std::iterator_traits< char* >

    template< class Iterator > class NormalizedCharIterator: public std::iterator_traits< Iterator >

    ОтветитьУдалить
  17. Угу. По большому счету, и сам D мог бы стать таким языком. Если бы у него автор был более прагматичным.

    запрещать во *всем* языке опасные фичи нельзя; в то же время, безопасный подъязык должен проверяться компилятором (хотя бы через ключ ком. строки)

    Производителям компиляторов следовало бы сохранять в объектных файлах/библиотеках параметры компиляции. А линкеру следовало бы проверять опции разных объектников/библиотек на совместимость.

    Хрен их дождешься. А при наличии своего языка с выхлопом в с++ можно было бы опции и дефайны в выхлопном файле кодировать в пространстве имен:

    using namespace _922048719823348337283;

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

    ОтветитьУдалить
  18. ЛОР стараниями модераторов теряет былую привлекательность и скатывается в Основной Продукт Жизнедеятельности; но все еще человек 20-30 полезных собеседников там есть.

    ОтветитьУдалить
  19. NB>>>а куда там заходить?

    ЕА>http://www.linux.org.ru/forum/development/

    спасиб. подписался, посмотрю чего народ пишет.

    ОтветитьУдалить
  20. 2имя:

    >Итераторное решение состоит из 2 классов, первый из которых можно найти кажется в буст::спирит (но я его накатал сам в 30 строк):

    А это решение можно где-нибудь увидеть?

    >А при наличии своего языка с выхлопом в с++ можно было бы опции и дефайны в выхлопном файле кодировать в пространстве имен:

    using namespace _922048719823348337283;


    Имхо, по-моему, компиляторы Eiffel так и делают.

    ОтветитьУдалить
  21. Да Хаскелисты на rsdn совсем мышей не ловят. Хотя они, и функциональщики вообще, теперь больше по блогам тусуются http://fprog.ru/planet/

    Вообще в функциоанльщине в этом году большие подвижки, зарелизился F# выйдет через месяц - другой OCaml 3.12 (язык очень существенно улучшается, поэтому совершенно непонятно как они нумеруют версии) и вот появился новый стандарт Хаскеля (интересно компилятор когда будет?)

    ОтветитьУдалить
  22. 2Rustam:

    >Вообще в функциоанльщине в этом году большие подвижки

    Ну, разбредание функциональщиков по блогам и подвижки могут говорить об одном из двух:

    - либо функциональщина таки проникает в мейнстрим;

    - либо этот пузырь все-таки сдувается.

    Скажу честно, меня устраивает любой из этих вариантов :)))

    ОтветитьУдалить
  23. - либо функциональщина таки проникает в мейнстрим;

    - либо этот пузырь все-таки сдувается.


    Одно другому не мешает, пузырек похоже уже прошел вторую фазу из http://en.wikipedia.org/wiki/Hype_cycle так что я жду в ближайшее время обличительных статей о том как функциональщики всех нае кхм обманули :)

    ОтветитьУдалить
  24. А это решение можно где-нибудь увидеть?

    щас оно просто скомпильнутое, но не падало и даже производило некую деятельность по замене; вообще выставить недоделанное как-то не очень... хотя х.з.

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

    писать код окна со строками мне влом; можно было бы сравнить "очные" ит 077;раторы с "заочным" окном, но хотя бы текстовую описаловку и объявления классов надо иметь "очно"

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

    З.Ы. "щас" это мое личное мнение о русском правописании, а не ошибка

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

    чем кстати ЛОР (и был) хорош -- там в критике не стесняются

    ОтветитьУдалить
  26. >смысл довести это до ума есть в том случае, если мы затеим дискуссию, ты (мне больше на ты нравится) поищешь в нем недостатки/сложности по сравнению с окном со строками и т.п. ... а тот вариант, что щас есть у меня, достаточен для того, чтобы мне сделать интересные для меня выводы

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

    ОтветитьУдалить