Доброе утро/день/вечер, 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(динамический тип), но с параметром по-умолчанию класса А (статический тип).
Вопрос по маркапу: как увеличить уровень отступа у абзаца текста и как продолжить нумерованный список?
Продолжение следует...
Очень забавный эффект. Когда-то нарвался на него выделив вектор вершин декартового дерева. Долго ловил RE7.
Интерестно! Все думаю, как же я ни разу на это не нарвался... Все таки С++ такая веревка:)
Если учесть, что семантически скопировать созданный дефолтным конструктором объект практически то же самое, что создать новый объект по дефолтному конструктору — вроде, пока нет хаков, все будет работать правильно.
Ну да, если вообще никогда не работать с динамической памятью, то все ок:)
Вообще, стараюсь по стилю практически этого и придерживаться :)
vector-ы вместо обычных массивов, делегаты owned by callee, object pool для хранения в сложных случаях, и глубокое копирование или refcount в крайних случаях — и явное управление временем жизни объекта достоинство, а не недостаток.
Интересный факт, который запомнился мне — с помощью const_cast, как известно, можно снимать квалификатор const и изменять переданный объект. Однако если передать ему указатель на глобальную константу — объект изменён не будет, но и ошибки не произойдет. Непонятно, зачем надо было делать столь противоречивую конструкцию в языке, если она имеет такой опасный эффект — тем более что не трудно сделать свой const_cast с тем же эффектом.
Кажется, by design сделать свой const_cast не должно быть возможно.
Да ну, каст в инт и обратно, например, вполне const_cast (за исключением того, что это undefined behavior, конечно). Я думаю, конст каст в с++ нужен, чтобы до таких костылей не доходило :) Иногда снятие const вполне логически валидно. Пример с декартовым деревом: чтобы получить значение элемента дерево можно разрезать и склеить обратно, при этом значения полей меняются, но метод можно считать не изменяющим дерево, потому что он обещает вернуть все на место.
Мне вот, конечно, совсем не хочется спорить с Мейерсом :) Но конструктор у вектора выглядит так:
explicit vector ( size_type n, const T& value= T(), const Allocator& = Allocator() );
То есть, если мы явно не указываем инициализирующий объект, то создается временный объект с конструктором по-умолчанию, и им потом инициализируются все объекты вектора. То есть на самом деле для каждого из элементов вектора будет вызван копирующий конструктор.
http://ideone.com/He40c
Возможно, оптимизатор может выбросить один из копирующих конструкторов, но это уже надо внимательно читать в стандарте про RVO.
В 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)
Да, действительно в Dinkum STL, которую использует Microsoft, конструкторы у вектора не совсем соотвествуют стандарту, по крайней мере, в 2005 студии, которая у меня есть под рукой. Делают они это скорее всего из-за какой-нибудь обратной совместимости. Но, что интересно, вывод у меня аналогичен выводу на ideone: один default и 5 copy конструкторов.
Вы правы. Это проверяется следующим кодом:
Вывод:
Default constructor calls: 1
Copy constructor calls: 10
То есть в каждый элемент вектора был скопирован какой-то временный default объект. Пост поправил.
Попробуйте сами ответить на вопрос почему, не заглядывая в спойлер. Довольно интересное упражнение :)
возможно, для кого то капитан, но пока мне это не рассказали, мне это не было так очевидно (либо я просто не задумывался об этом)
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)
а есть ли смысл вообще делать delete в решениях задач?
Да, если у вас ML, а TL-я очень много. Или программа тормозит из-за слишком большого количества памяти.Да и не только олимпиады же бывают в конце концов.
мне не понятно зачем вообще работать с динамической памятью в олимпиадах?
Как мне рассказывали: выделение памяти может быть долгим, выделяется больше памяти, чем может быть нужно, куча багов вылазит на выделении, если руки кривые
Выделять память статически и динамически занимает примерно одинаковое время. А если руки кривые, то куча багов вылазит где угодно. Тормозить скорее может то, что при динамическом выделении данные могут быть разбросаны по памяти, и по ней приходится бегать. А по поводу зачем. При реализации некоторых структур данных указатели удобнее, чем индексы. А комбинация статического массива с указателями выглядит как-то странно на мой взгляд. Хотя это чисто вопрос привычки. И еще бывает когда нужно как-то нетривиально выделить внутреннюю размерность массива. Тут уже без динамической памяти не обойтись.
Кстати, как рассказывал e-maxx , если для treap выделять память динамически, можно использовать в качестве приоритета адрес структуры в памяти.
Это конечно да, но у меня нет уверенности, что стандартное выделение памяти не старается быть последовательным по мере возможности.
попробовал только что это сдать, тле...
Странно, но у меня было несколько случаев, когда решение не проходило по ТЛ, а при замене динамической памяти на статическую, все летало в 2-3 раза быстрее.
Это в основном задачи на Treap, Trie, AhoCorasick алгоритмы.
Я уже вроде объяснил. Тормозить скорее может то, что при динамическом выделении данные могут быть разбросаны по памяти, и по ней приходится бегать.
и такой вопрос: говорят есть неплохой хак на замену динамической памяти на статику: перезагрузить глобальный new под свои нужды, кто-нибудь умеет такое?
Почитай про placement new для начала, может не понадобиться new перегружать.
ну мне это недавно понадобилось, я писал трип, а в задаче были мультитесты, и если вот так не
delete
для каждого теста, памяти много ушло бы, возможно даже MLE былЯ обычно беру вершины трипа (равно как декартова дерева) из глобального массива (хоть на указателях, хоть на индексах) и просто сбрасываю счетчик выделенных вершин в начале каждого теста. Писать почти не больше, можно не волноваться об оверхеде new, и не надо все обходить, чтобы удалить (правда, удалять все равно почти никогда не нужно).
по поводу третьего. если данные входные имеют такой вид.
3
sltkjsalktjlktj то после тройки остается еще символ перевода строки "enter". поэтому getline(cin,s) сосчитает как раз enter.
getline(cin, s) будет считывать символы из файла, пока не считает перевод строки. Пусть у тебя файл '7' 'abacaba' 'cin >> n' считает 7, но не считает перевод строки и таким образом первый символ, который считает getline(cin, s) это и будет перевод строки.
я тоже было так думал, но вот код, и если бы он считал в
s
перевод строки, у нас бы # была на следующей строке или я неправ?P.S. вопрос закрыт
Он читает до перевода строки, и считывает перевод строки в никуда. Зачем он нужен в строке?
P.S. А почему я считал что здесь с таким ником ilona а не ты?
ты меня спрашиваешь?) ну знай теперь
Про 3 пункт, да это вызывается тем, что число считывается только до перевода строки, пробела, но следующий символ не изчезает. Это решается 2 способами:
1) Простой: fflush(stdin) перед каждым чтением строки, просто очищает консоль от всяких переводов строки и пробелов
2) Джедайский: при помощи функции scanf(), считываем все числа scanf("%d*c",&n);. Этот код считывает число и следующий символ. Также кошерным будет читать строку через
scanf(" %[^\n]", s);
P.S. Код не вставляется по нормальному, так что терпите.
В новом стандарте, с введением rvalue-ссылок, s=s+t будет столь же быстрой, как и s+=t
Второй пункт показался несколько не к месту. Но это конечно субъективно. Вот что меня в 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 //удачного дебага су%и
очень весело дебажить такой код, слава богу на своей шкуре я такого ада не испытывал, но очевидцы говрят: "[вырезано цензурой]", ну и кроме мата только предлоги.
На правах оффтопа. Мне больше нравится
#define i j
#define return return rand()%1000<=10?rand():
?А вы, случаем, не в MSVC последний пример делали?
нет, это под линукс, у студии я такого не встречал.
Мой любимый вопрос про указатели на функции-члены класса -- это как отличить указатель на виртуальную функцию от указателя на невиртуальную функцию.
Вот это код выводит:
Последние две строки ожидаемы -- все вызвалось верно.
Первая строка говорит, что указатели на функцию обе 16 байт, вторая говорит, что последние восемь байт в обоих указателях не используются, в то время как первые восемь байт в первом указателе содержат номер в виртуальной таблице, а во втором -- адрес функции.
Так как тип у них одинаковый, вопрос -- как при вызове компилятор понимает, вызывать по адресу или из таблицы?
Понимает не компилятор, а в рантайме это происходит, не? Видимо размер таблицы чем-то ограничен, и там что-то вроде
if (address < (1 << 16)) return call table_bla_bla[address] else call address
;Да, имеллось ввиду как в рантайме определяется что вызвать.
На самом деле примерно так и сделано как ты говоришь, только я не думаю, что таблица на самом деле ограницена. Просто если за ее пределы вылезти, скорее всего поймаешь AV попытавшись вызвать виртуальный метод через указатель.
Когда я писал суфавтомат сегодня первый раз, я, следуя заветам e-maxx, объявил структуру вида
const int MAXLEN = 100000;
struct SuffixAutomata {
state st[2 * MAXLEN];
//code here
}
Я пишу в Visual Studio 2010 и когда я запустил прогу, она повисла. Немного подебугав, я понял, что дело в размере массива st. Изменив его на 100, все стало летать моментально. Я пробовал и под Debug, и под Release, и внутри структуры массив объявлять, и вне — эффект одинаковый.
В чем тут дело — в необычной работе Студии, тонкости с вызовом конструкторов для массивов объектов или чем-то еще?
VS2010 особенно тупая IDE, и прога на зависла, она просто очень мееееееееееееееееееееееееееееееееееееееееееееееееееееееедленно работала. Видимо, VS пихает в конструкторы исключительно много debug-кода. Это легко заметить, запустив свою программу комбинацией ctrl-f5, должно быть принципиально быстрее.
ну пихание дебаг кода в дебаге это скорее плюс, чем минус, ибо в C++ вообще сложно отследить многие ошибки, а MS постарались за ними следить.
По-моему, было бы лучше не дописывать новые факты в этот пост, а создавать новые: сейчас трудно разобрать, какой комментарий к чему относится. Думаю, польза от релевантных комментариев перевесит вред от засорения прямого эфира :)
В С++ с параметрами по умолчанию есть еще одна довольно спорная возможность, которую (и слава богу) не знает подавляющее большинство С++ программистов, да и, по правде говоря, я думаю, что не все из экспертов об этом в курсе. Во всяком случае, таким примером у меня почти всегда получалось удивить собеседника :) http://ideone.com/A5Hdv
Можете пояснить предпоследний результат?
Если честно, то я не очень понимаю, что особенного именно в предпоследнем результате. Там сначала делается еще одно объявление функции
int foo(int a = 3, int b, int c);
, которое добавляет еще одно значение по умолчанию теперь уже для первого параметра. Затем, так как в текущей области видимости уже указаны значения по умолчанию для абсолютно всех параметров, то мы можем просто вызвать функцию foo, не передавая туда ни одного параметра, а результат будет соответствовать указанным значениям по умолчанию