• Локальный класс не может иметь статических данных-членов.






  • C++: Under the Hood. Она описывает реализацию, использованную разработчиками MSVC.





    Стр.382: 13.2.3. Параметры шаблонов



    В частности, строковый литерал не допустим в качестве аргумента шаблона.


    Потому что строковый литерал -- это объект с внутренней компоновкой (internal linkage).





    Стр.399: 13.6.2. Члены-шаблоны



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


    М-да... Определенно, не самое удачное место русского перевода. Тем более, что в оригинале все предельно просто и понятно:


    Curiously enough, a template constructor is never used to generate a copy constructor, so without the explicitly declared copy constructor, a default copy constructor would have been generated.


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



    Далее хочу отметить, что постоянно встречающуюся в переводе фразу "конструктор шаблона" следует понимать как "конструктор-шаблон".





    Стр.419: 14.4.1. Использование конструкторов и деструкторов



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


    Если вы решили, что тем самым должна повыситься производительность, ввиду того, что в теле функции отсутствуют блоки try/catch, то должен вас огорчить -- они будут автоматически сгенерированы компилятором для корректной обработки раскрутки стека. Но все-таки, какая версия выделения ресурсов обеспечивает большую производительность? Давайте протестируем следующий код:

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>

    void ResourceAcquire();
    void ResourceRelease();
    void Work();

    struct RAII {
    RAII() { ResourceAcquire(); }
    ~RAII() { ResourceRelease(); }
    };

    void f1()
    {
    ResourceAcquire();

    try { Work(); }
    catch (...) {
    ResourceRelease();
    throw;
    }

    ResourceRelease();
    }

    void f2()
    {
    RAII raii;
    Work();
    }


    long Var, Count;

    void ResourceAcquire() { Var++; }
    void ResourceRelease() { Var--; }
    void Work() { Var+=2; }

    int main(int argc, char** argv)
    {
    if (argc>1) Count=atol(argv[1]);

    clock_t c1, c2;
    {
    c1=clock();

    for (long i=0; i<Count; i++)
    for (long j=0; j<1000000; j++)
    f1();

    c2=clock();
    printf("f1(): %ld mln calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK);
    }
    {
    c1=clock();

    for (long i=0; i<Count; i++)
    for (long j=0; j<1000000; j++)
    f2();

    c2=clock();
    printf("f2(): %ld mln calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK);
    }
    }

    Как выдумаете, какая функция работает быстрее? А вот и нет! В зависимости от компилятора быстрее работает то f1(), то f2(), а иногда они работают совершенно одинаково из-за полной идентичности сгенерированного компилятором кода. Все зависит от используемых принципов обработки исключений и качества оптимизатора.


    Как же работают исключения? Если вкратце, то в разных реализациях исключения работают по-разному. И всегда чрезвычайно нетривиально! Особенно много сложностей возникает с ОС, использующими так называемый Structured Exception Handling и/или поддерживающими многопоточность (multithreading). Фактически, с привычными нам современными ОС...


    На текущий момент в Internet можно найти достаточное количество материала по реализации exception handling (EH) в C++ и не только, приводить здесь который не имеет особого смысла. Тем не менее, влияние EH на производительность C++ программ заслуживает отдельного обсуждения.


    Увы, но стараниями недобросовестных "преувеличителей достоинств" в массы пошел миф о том, что обработку исключений можно реализовать вообще без накладных расходов. На самом деле это не так, т.к. даже самый совершенный метод реализации EH, отслеживающий созданные (и, следовательно, подлежащие уничтожению) на данный момент (под)объекты по значению счетчика команд (например, регистр (E)IP процессоров Intel-архитектуры) не срабатывает в случае создания массивов.


    Но более надежным (и, кстати, не зависящим от способа реализации EH) опровержением исходной посылки является тот факт, что EH добавляет дополнительные дуги в Control Flow Graph, т.е. в граф потоков управления, что не может не сказаться на возможностях оптимизаци.


    Тем не менее, накладные расходы на EH в лучших реализациях не превышают 5%, что с практической точки зрения почти эквивалентно полному отсутствию расходов.


    Но это в лучших реализациях! О том, что происходит в реализациях "обычных" лучше не упоминать -- как говорит герой известного анекдота: "Гадкое зрелище"...





    Стр.421: 14.4.2. auto_ptr



    В стандартном заголовочном файле <memory> auto_ptr объявлен следующим образом...


    Ввиду того, что после выхода первых (английских) тиражей стандарт претерпел некоторые изменения в части auto_ptr, концовку данного раздела следует заменить следующим текстом (он взят из списка авторских исправлений к 4 тиражу).


    Для достижения данной семантики владения (также называемой семантикой разрушающего копирования (destructive copy semantics)), семантика копирования шаблона auto_ptr радикально отличается от семантики копирования обычных указателей: когда один auto_ptr копируется или присваивается другому, исходный auto_ptr очищается (эквивалентно присваиванию 0 указателю). Т.к. копирование auto_ptr приводит к его изменению, то const auto_ptr не может быть скопирован.


    Шаблон auto_ptr определен в <memory> следующим образом:

    template<class X> class std::auto_ptr {
    // вспомогательный класс
    template <class Y> struct auto_ptr_ref { /* ... */ };

    X* ptr;
    public:
    typedef X element_type;

    explicit auto_ptr(X* p =0) throw() { ptr=p; }
    ~auto_ptr() throw() { delete ptr; }

    // обратите внимание: конструкторы копирования и операторы
    // присваивания имеют неконстантные аргументы

    // скопировать, потом a.ptr=0
    auto_ptr(auto_ptr& a) throw();

    // скопировать, потом a.ptr=0
    template<class Y> auto_ptr(auto_ptr<Y>& a) throw();

    // скопировать, потом a.ptr=0
    auto_ptr& operator=(auto_ptr& a) throw();

    // скопировать, потом a.ptr=0
    template<class Y> auto_ptr& operator=(auto_ptr<Y>& a) throw();

    X& operator*() const throw() { return *ptr; }
    X* operator->() const throw() { return ptr; }

    // вернуть указатель
    X* get() const throw() { return ptr; }

    // передать владение
    X* release() throw() { X* t = ptr; ptr=0; return t; }

    void reset(X* p =0) throw() { if (p!=ptr) { delete ptr; ptr=p; } }

    // скопировать из auto_ptr_ref
    auto_ptr(auto_ptr_ref<X>) throw();

    // скопировать в auto_ptr_ref
    template<class Y> operator auto_ptr_ref<Y>() throw();

    // разрушающее копирование из auto_ptr
    template<class Y> operator auto_ptr<Y>() throw();
    };

    Назначение auto_ptr_ref -- обеспечить семантику разрушающего копирования, ввиду чего копирование константного auto_ptr становится невозможным. Конструктор-шаблон и оператор присваивания-шаблон обеспечивают возможность неявного пребразования auto_ptr<D> в auto_ptr<B> если D* может быть преобразован в B*, например:
    void g(Circle* pc)
    {
    auto_ptr<Circle> p2 = pc; // сейчас p2 отвечает за удаление

    auto_ptr<Circle> p3 = p2; // сейчас p3 отвечает за удаление,
    // а p2 уже нет

    p2->m = 7; // ошибка программиста: p2.get()==0

    Shape* ps = p3.get(); // извлечение указателя

    auto_ptr<Shape> aps = p3; // передача прав собственности и
    // преобразование типа

    auto_ptr<Circle> p4 = pc; // ошибка: теперь p4 также отвечает за удаление
    }

    Эффект от использования нескольких auto_ptr для одного и того же объекта неопределен; в большинстве случаев объект будет уничтожен дважды, что приведет к разрушительным результатам.


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

    // опасно: использование auto_ptr в контейнере
    void h(vector<auto_ptr<Shape> >& v)
    {
    sort(v.begin(),v.end()); // не делайте так: элементы не будут отсортированы
    }

    Понятно, что auto_ptr не является обычным "умным" указателем, однако он прекрасно справляется с предоставленной ему ролью -- обеспечивать безопасную относительно исключений работу с автоматическими указателями, и делать это без существенных накладных расходов.





    Стр.422: 14.4.4. Исключения и оператор new



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


    Т.к. приведенные в книге объяснения немного туманны, вот соответствующая часть стандарта:


    5.3.4. New [expr.new]



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







    Стр.431: 14.6.1. Проверка спецификаций исключений



    Спецификация исключений не является частью типа функции, и typedef не может ее содержать.


    Сразу же возникает вопрос: в чем причина этого неудобного ограничения? Д-р Страуструп пишет по этому поводу следующее:


    The reason is the exception spacification is not part of the type; it is a constraint that is checked on assignment and exforced at run time (rather than at compile time). Some people would like it to be part of the type, but it isn't. The reason is to avoid difficulties when updating large systems with parts from different sources. See "The Design and Evolution of C++" for details.


    Причина в том, что спецификации исключений не являются частью типа; данное ограничение проверяется при присваивании и принудительно обеспечивается во время выполнения (а не во время компиляции). Некоторым людям хотелось бы, чтобы спецификации исключений были частью типа, но это не так. Причина в том, что мы хотим избежать трудностей, возникающих при внесении изменений в большие системы, состоящие из отдельных частей полученных из разных источников. Обратитесь к книге "Дизайн и эволюция C++" за деталями.



    По моему мнению, спецификации возбуждаемых исключений -- это одна из самых неудачных частей определения C++. Исторически, неадекватность существующего механизма спецификации исключений обусловлена отсутствием реального опыта систематического применения исключений в C++ (и возникающих при этом вопросов exception safety) на момент их введения в определение языка. К слову сказать, о сложности проблемы говорит и тот факт, что в Java, появившемся заметно позже C++, спецификации возбуждаемых исключений так же реализованы неудачно.


    Имеющийся на текущий момент опыт свидетельствует о том, что критически важной для написания exception safe кода информацией является ответ на вопрос: Может ли функция вообще возбуждать исключения? Эта информация известна уже на этапе компиляции и может быть проверена без особого труда.


    Так, например, можно ввести ключевое слово nothrow:



    •  // ключевое слово nothrow отсутствует:
      // f() разрешено возбуждать любые исключения прямо или косвенно
      void f()
      {
      // ...
      }



    •  // f() запрещено возбуждать любые исключения прямо или косвенно,
      // проверяется на этапе компиляции
      void f() nothrow
      {
      // ...
      }



    •  void f()
      {

      // здесь можно возбуждать исключения прямо или косвенно

      nothrow { // nothrow-блок

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

      }

      // здесь снова можно возбуждать исключения

      }



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