Блог пользователя Fefer_Ivan

Автор Fefer_Ivan, 12 лет назад, По-русски

Доброе утро/день/вечер, Codeforces.

В этом посте я буду писать о тонкостях С++, которые лично мне кажутся интересными. Постараюсь выкладывать тонкости по одной штуке раз в 2 дня. Также, я надеюсь на то, что сообщество будет делиться своими знаниями С++ в комментариях. Ну, приступим:

  • 08.02.2012. Источник: С. Мейерс — Эффективное использование STL.
    Когда вы создаете статический массив из N объектов класса A (A arr[N]), для каждого элемента массива вызывается default конструктор. А когда вы создаете вектор из N объектов класса А (vector<A> arr(N)), то при помощи default конструктора будет создан временный объект, а для всех элементов массива будет вызван конструктор копирования от этого временного объекта.

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

    srand(12345);
    random_shuffle(a, a + n);
    srand(12345);
    random_shuffle(b, b + n);

Извините, что не обновлял тему уже 7 дней. Мне было необходимо сдать экзамен по численным методам, который я пропустил из-за Петрозаводска. Сдал на 5 и теперь с чистой совестью продолжаю.

  • 18.02.2012. Источник: С. Мейерс — Эффективное использование C++.
    Тонкость не совсем олимпиадная, но очень интересная. Рассмотрим следующий код:
    #include <cstdio>

    class A{
    public:
        virtual void print(int p = 1){
            printf("A %d\n", p);
        }
    };

    class B : public A{
    public:
        virtual void print(int p = 30){
            printf("B %d\n", p);
        }
    };


    int main(){

        A* a = new A();
        a->print(); //Используется параметр по-умолчанию класса А, т.е. 1
        A* b = new B();
        b->print(); //Кажется, что используется параметр по-умолчанию класса B, т.е. 30

        return 0;
    }    

А теперь результат его исполнения: http://ideone.com/DcY4o.
При вызове виртуальной функции, функция выбирается в зависимости от динамического типа (то есть от типа объекта на который в данный момент указывает указатель), а параметры по-умолчанию — от статического типа (то есть определенного в момент объявления). По-этому, когда мы пишем A* b = new B(); b->print();, мы вызываем функцию класса B(динамический тип), но с параметром по-умолчанию класса А (статический тип).

Вопрос по маркапу: как увеличить уровень отступа у абзаца текста и как продолжить нумерованный список?

Продолжение следует...

  • Проголосовать: нравится
  • +78
  • Проголосовать: не нравится

»
12 лет назад, # |
  Проголосовать: нравится +13 Проголосовать: не нравится

Очень забавный эффект. Когда-то нарвался на него выделив вектор вершин декартового дерева. Долго ловил RE7.

»
12 лет назад, # |
  Проголосовать: нравится +29 Проголосовать: не нравится

Интерестно! Все думаю, как же я ни разу на это не нарвался... Все таки С++ такая веревка:)

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится +8 Проголосовать: не нравится

    Если учесть, что семантически скопировать созданный дефолтным конструктором объект практически то же самое, что создать новый объект по дефолтному конструктору — вроде, пока нет хаков, все будет работать правильно.

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      Ну да, если вообще никогда не работать с динамической памятью, то все ок:)

      • »
        »
        »
        »
        12 лет назад, # ^ |
        Rev. 2   Проголосовать: нравится +5 Проголосовать: не нравится

        Вообще, стараюсь по стилю практически этого и придерживаться :)

        vector-ы вместо обычных массивов, делегаты owned by callee, object pool для хранения в сложных случаях, и глубокое копирование или refcount в крайних случаях — и явное управление временем жизни объекта достоинство, а не недостаток.

»
12 лет назад, # |
  Проголосовать: нравится +3 Проголосовать: не нравится

Интересный факт, который запомнился мне — с помощью const_cast, как известно, можно снимать квалификатор const и изменять переданный объект. Однако если передать ему указатель на глобальную константу — объект изменён не будет, но и ошибки не произойдет. Непонятно, зачем надо было делать столь противоречивую конструкцию в языке, если она имеет такой опасный эффект — тем более что не трудно сделать свой const_cast с тем же эффектом.

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Кажется, by design сделать свой const_cast не должно быть возможно.

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      Да ну, каст в инт и обратно, например, вполне const_cast (за исключением того, что это undefined behavior, конечно). Я думаю, конст каст в с++ нужен, чтобы до таких костылей не доходило :) Иногда снятие const вполне логически валидно. Пример с декартовым деревом: чтобы получить значение элемента дерево можно разрезать и склеить обратно, при этом значения полей меняются, но метод можно считать не изменяющим дерево, потому что он обещает вернуть все на место.

»
12 лет назад, # |
Rev. 2   Проголосовать: нравится +19 Проголосовать: не нравится

Мне вот, конечно, совсем не хочется спорить с Мейерсом :) Но конструктор у вектора выглядит так: explicit vector ( size_type n, const T& value= T(), const Allocator& = Allocator() );

То есть, если мы явно не указываем инициализирующий объект, то создается временный объект с конструктором по-умолчанию, и им потом инициализируются все объекты вектора. То есть на самом деле для каждого из элементов вектора будет вызван копирующий конструктор.

http://ideone.com/He40c

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

  • »
    »
    12 лет назад, # ^ |
    Rev. 4   Проголосовать: нравится +3 Проголосовать: не нравится

    В MS куча перегруженных конструкторов вектора. В том числе с одним параметром — размером.

    Кстати, вывод MSC на 5 элементов весьма необычен:
    A::A()
    A::A(const& A)
    A::A()
    A::A(const& A)
    A::A()
    A::A(const& A)
    A::A()
    A::A(const& A)
    A::A()
    A::A(const& A)

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      Да, действительно в Dinkum STL, которую использует Microsoft, конструкторы у вектора не совсем соотвествуют стандарту, по крайней мере, в 2005 студии, которая у меня есть под рукой. Делают они это скорее всего из-за какой-нибудь обратной совместимости. Но, что интересно, вывод у меня аналогичен выводу на ideone: один default и 5 copy конструкторов.

  • »
    »
    12 лет назад, # ^ |
    Rev. 2   Проголосовать: нравится +3 Проголосовать: не нравится

    Вы правы. Это проверяется следующим кодом:

    #include <cstdio>
    #include <vector>
    
    using namespace std;
    
    int c, d;
    
    struct A{
        A(){ d++; }
        A(const A& a) { c++; }
    };
    int main(){
        vector<A> v(10);
    
        printf("Default constructor calls: %d\n", d);
        printf("Copy constructor calls: %d\n", c);       
        
        return 0;
    }
    
    

    Вывод:

    Default constructor calls: 1

    Copy constructor calls: 10

    То есть в каждый элемент вектора был скопирован какой-то временный default объект. Пост поправил.

»
12 лет назад, # |
Rev. 2   Проголосовать: нравится +17 Проголосовать: не нравится

template<class T> void f(T); // 1 template<class T> void f(T*); // 2 template<> void f<int*>(int*);// 3 ... int a; f(&a); // Вызывается функция 2.

Попробуйте сами ответить на вопрос почему, не заглядывая в спойлер. Довольно интересное упражнение :)

»
12 лет назад, # |
Rev. 15   Проголосовать: нравится +9 Проголосовать: не нравится

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

1) когда вы конкатенируете строки, то запись вида s = s + t работает за O(|s| + |t|) то есть создастся новая строка s+t и только потом присвоится s, а s += t за O(|t|)

2) допустим мы делаем следующую вещь delete v; (где v — например вершина treap), то v — удаляется, но в NULL не обращается

3) наверное знакомое каждому сишнику, но я лично не понимаю, почему так происходит: если написать примерно следующее:

int n;

string s;

cin >> n;

getline(cin, s);

то строка s не считается, а считается она, если двоекратно написать getline(cin, s)

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится +11 Проголосовать: не нравится

    а есть ли смысл вообще делать delete в решениях задач?

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится +8 Проголосовать: не нравится

      Да, если у вас ML, а TL-я очень много. Или программа тормозит из-за слишком большого количества памяти.Да и не только олимпиады же бывают в конце концов.

      • »
        »
        »
        »
        12 лет назад, # ^ |
          Проголосовать: нравится +3 Проголосовать: не нравится

        мне не понятно зачем вообще работать с динамической памятью в олимпиадах?

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

        • »
          »
          »
          »
          »
          12 лет назад, # ^ |
            Проголосовать: нравится +1 Проголосовать: не нравится

          Выделять память статически и динамически занимает примерно одинаковое время. А если руки кривые, то куча багов вылазит где угодно. Тормозить скорее может то, что при динамическом выделении данные могут быть разбросаны по памяти, и по ней приходится бегать. А по поводу зачем. При реализации некоторых структур данных указатели удобнее, чем индексы. А комбинация статического массива с указателями выглядит как-то странно на мой взгляд. Хотя это чисто вопрос привычки. И еще бывает когда нужно как-то нетривиально выделить внутреннюю размерность массива. Тут уже без динамической памяти не обойтись.

          • »
            »
            »
            »
            »
            »
            12 лет назад, # ^ |
              Проголосовать: нравится 0 Проголосовать: не нравится

            Кстати, как рассказывал e-maxx , если для treap выделять память динамически, можно использовать в качестве приоритета адрес структуры в памяти.

            • »
              »
              »
              »
              »
              »
              »
              12 лет назад, # ^ |
                Проголосовать: нравится 0 Проголосовать: не нравится

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

            • »
              »
              »
              »
              »
              »
              »
              12 лет назад, # ^ |
                Проголосовать: нравится 0 Проголосовать: не нравится

              попробовал только что это сдать, тле...

          • »
            »
            »
            »
            »
            »
            12 лет назад, # ^ |
              Проголосовать: нравится 0 Проголосовать: не нравится

            Странно, но у меня было несколько случаев, когда решение не проходило по ТЛ, а при замене динамической памяти на статическую, все летало в 2-3 раза быстрее.

            Это в основном задачи на Treap, Trie, AhoCorasick алгоритмы.

            • »
              »
              »
              »
              »
              »
              »
              12 лет назад, # ^ |
                Проголосовать: нравится 0 Проголосовать: не нравится

              Я уже вроде объяснил. Тормозить скорее может то, что при динамическом выделении данные могут быть разбросаны по памяти, и по ней приходится бегать.

          • »
            »
            »
            »
            »
            »
            12 лет назад, # ^ |
              Проголосовать: нравится 0 Проголосовать: не нравится

            и такой вопрос: говорят есть неплохой хак на замену динамической памяти на статику: перезагрузить глобальный new под свои нужды, кто-нибудь умеет такое?

            • »
              »
              »
              »
              »
              »
              »
              12 лет назад, # ^ |
                Проголосовать: нравится 0 Проголосовать: не нравится

              Почитай про placement new для начала, может не понадобиться new перегружать.

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      ну мне это недавно понадобилось, я писал трип, а в задаче были мультитесты, и если вот так не delete для каждого теста, памяти много ушло бы, возможно даже MLE был

      • »
        »
        »
        »
        12 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится

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

  • »
    »
    12 лет назад, # ^ |
    Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

    по поводу третьего. если данные входные имеют такой вид.
    3
    sltkjsalktjlktj то после тройки остается еще символ перевода строки "enter". поэтому getline(cin,s) сосчитает как раз enter.

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    getline(cin, s) будет считывать символы из файла, пока не считает перевод строки. Пусть у тебя файл '7' 'abacaba' 'cin >> n' считает 7, но не считает перевод строки и таким образом первый символ, который считает getline(cin, s) это и будет перевод строки.

    • »
      »
      »
      12 лет назад, # ^ |
      Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

      я тоже было так думал, но вот код, и если бы он считал в s перевод строки, у нас бы # была на следующей строке или я неправ?

      P.S. вопрос закрыт

      • »
        »
        »
        »
        12 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится

        Он читает до перевода строки, и считывает перевод строки в никуда. Зачем он нужен в строке?

        P.S. А почему я считал что здесь с таким ником ilona а не ты?

        • »
          »
          »
          »
          »
          12 лет назад, # ^ |
            Проголосовать: нравится 0 Проголосовать: не нравится

          ты меня спрашиваешь?) ну знай теперь

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Про 3 пункт, да это вызывается тем, что число считывается только до перевода строки, пробела, но следующий символ не изчезает. Это решается 2 способами:

    1) Простой: fflush(stdin) перед каждым чтением строки, просто очищает консоль от всяких переводов строки и пробелов

    2) Джедайский: при помощи функции scanf(), считываем все числа scanf("%d*c",&n);. Этот код считывает число и следующий символ. Также кошерным будет читать строку через

    scanf(" %[^\n]", s);

    P.S. Код не вставляется по нормальному, так что терпите.

  • »
    »
    12 лет назад, # ^ |
    Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

    1) когда вы конкатенируете строки, то запись вида s = s + t работает за O(|s| + |t|) то есть создастся новая строка s+t и только потом присвоится s, а s += t за O(|t|)

    В новом стандарте, с введением rvalue-ссылок, s=s+t будет столь же быстрой, как и s+=t

»
12 лет назад, # |
Rev. 4   Проголосовать: нравится 0 Проголосовать: не нравится

Второй пункт показался несколько не к месту. Но это конечно субъективно. Вот что меня в C++ радовало так это виртуальное наследование, в котором у виртуального класса помимо указателя на таблицу виртуальных функций, появляется еще один. Ещё одна особенность, взрывающая мне мозг, это указатели на члены классов, тут ваще песня, правда я с этим геморроем не разбирался в достаточной степени, дабы не свихнуться. Указатель на функцию-член класса занимает от 4-х до X (вроде 24) байт в зависимости от модификаторов, виртуальности, положения планет, разрядности системы. О темплейтах даже упоминать не стоит, на это есть александреску, пусть у него мозги кипят. Меня очень порадовало, что берётся первая "реализованная" реализация темплейта в одном проекте, вне зависимости от включенных хидеров. Понимая, что предыдущее предложение сложно понять поясняю примером: a.h: template<class T> int F(T x) {return 42;}
b.h: template<class T> int F(T x) {return 666;}
a.cpp: include<a.h> ... F<int>(0)...
b.cpp: include<b.h> ... F<int>(0)...
main.cpp: cout << F(0) << endl
main.cpp выведет 42, если a.cpp будет скомпилен раньше, чем b.cpp, 666 иначе. Но это ещё фигня, вот если к классу прилинкуется деструктор от другого класса, вот тут что говорится #define true false //удачного дебага су%и
очень весело дебажить такой код, слава богу на своей шкуре я такого ада не испытывал, но очевидцы говрят: "[вырезано цензурой]", ну и кроме мата только предлоги.

  • »
    »
    12 лет назад, # ^ |
    Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

    #define true false

    На правах оффтопа. Мне больше нравится #define i j

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    А вы, случаем, не в MSVC последний пример делали?

  • »
    »
    12 лет назад, # ^ |
    Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

    Мой любимый вопрос про указатели на функции-члены класса -- это как отличить указатель на виртуальную функцию от указателя на невиртуальную функцию.

    #include <stdio.h>
    
    class Moo
    {
        public:
            virtual void a1(int a) { printf("a1 %d\n", a); };
            void a2(int a) { printf("a2 %d\n", a); };
    };
    
    int main()
    {
        Moo* a = new Moo;
    
        void(Moo::*A1)(int) = &Moo::a1;
        void(Moo::*A2)(int) = &Moo::a2;
    
        printf("SizeOfs: %d %d\n", sizeof(A1), sizeof(A2));
    
        printf("Content of A1: %lld %lld\n", A1);
        printf("Content of A2: %lld %lld\n", A2);
    
        (a->*A1)(5);
        (a->*A2)(7);
    
        return 0;
    }
    
    

    Вот это код выводит:

    SizeOfs: 16 16
    Content of A1: 1 0
    Content of A2: 4196196 0
    a1 5
    a2 7
    

    Последние две строки ожидаемы -- все вызвалось верно.

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

    Так как тип у них одинаковый, вопрос -- как при вызове компилятор понимает, вызывать по адресу или из таблицы?

    • »
      »
      »
      12 лет назад, # ^ |
      Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

      Понимает не компилятор, а в рантайме это происходит, не? Видимо размер таблицы чем-то ограничен, и там что-то вроде if (address < (1 << 16)) return call table_bla_bla[address] else call address;

      • »
        »
        »
        »
        12 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится

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

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

»
12 лет назад, # |
Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

Когда я писал суфавтомат сегодня первый раз, я, следуя заветам e-maxx, объявил структуру вида
const int MAXLEN = 100000;
struct SuffixAutomata {
state st[2 * MAXLEN];
//code here
}
Я пишу в Visual Studio 2010 и когда я запустил прогу, она повисла. Немного подебугав, я понял, что дело в размере массива st. Изменив его на 100, все стало летать моментально. Я пробовал и под Debug, и под Release, и внутри структуры массив объявлять, и вне — эффект одинаковый.

В чем тут дело — в необычной работе Студии, тонкости с вызовом конструкторов для массивов объектов или чем-то еще?

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится +7 Проголосовать: не нравится

    VS2010 особенно тупая IDE, и прога на зависла, она просто очень мееееееееееееееееееееееееееееееееееееееееееееееееееееееедленно работала. Видимо, VS пихает в конструкторы исключительно много debug-кода. Это легко заметить, запустив свою программу комбинацией ctrl-f5, должно быть принципиально быстрее.

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится +4 Проголосовать: не нравится

      ну пихание дебаг кода в дебаге это скорее плюс, чем минус, ибо в C++ вообще сложно отследить многие ошибки, а MS постарались за ними следить.

»
12 лет назад, # |
  Проголосовать: нравится +26 Проголосовать: не нравится

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

»
12 лет назад, # |
  Проголосовать: нравится +31 Проголосовать: не нравится

В С++ с параметрами по умолчанию есть еще одна довольно спорная возможность, которую (и слава богу) не знает подавляющее большинство С++ программистов, да и, по правде говоря, я думаю, что не все из экспертов об этом в курсе. Во всяком случае, таким примером у меня почти всегда получалось удивить собеседника :) http://ideone.com/A5Hdv

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Можете пояснить предпоследний результат?

    • »
      »
      »
      12 лет назад, # ^ |
      Rev. 2   Проголосовать: нравится +1 Проголосовать: не нравится

      Если честно, то я не очень понимаю, что особенного именно в предпоследнем результате. Там сначала делается еще одно объявление функции int foo(int a = 3, int b, int c);, которое добавляет еще одно значение по умолчанию теперь уже для первого параметра. Затем, так как в текущей области видимости уже указаны значения по умолчанию для абсолютно всех параметров, то мы можем просто вызвать функцию foo, не передавая туда ни одного параметра, а результат будет соответствовать указанным значениям по умолчанию