вторник, 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 комментариев:

имя комментирует...

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

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

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

Анонимный комментирует...

> board.rt.mipt.ru.

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

имя комментирует...

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

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

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

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

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

имя комментирует...
Этот комментарий был удален автором.
имя комментирует...

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

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

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

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

имя комментирует...

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

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

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

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

eao197 комментирует...

2имя:

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

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

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

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

eao197 комментирует...

2имя:

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

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

eao197 комментирует...

2san:

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

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

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

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

night beast комментирует...

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

Objective C?

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

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

eao197 комментирует...

>Objective C?

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

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

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

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

Rustam комментирует...

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

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

eao197 комментирует...

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

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

eao197 комментирует...

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

имя комментирует...

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

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

Впрочем, окошко еще видимо можно спасти: делаем класс 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-у найденной лицензии

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

имя комментирует...

блин, вырезало "тэги"

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

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

имя комментирует...

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

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

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

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

using namespace _922048719823348337283;

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

имя комментирует...

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

night beast комментирует...

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

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

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

eao197 комментирует...

2имя:

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

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

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

using namespace _922048719823348337283;


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

Rustam комментирует...

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

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

eao197 комментирует...

2Rustam:

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

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

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

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

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

Rustam комментирует...

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

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


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

имя комментирует...

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

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

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

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

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

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

имя комментирует...

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

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

eao197 комментирует...

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

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