Простые заметки про Си++

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

Const и методы - указатели

Константный метод, возвращающий указатель на элемент класса.

  1. char * GetName ( ) const { return pName; }

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

Поэтому для по настоящему константных методов, возвращающих указатель нужно использовать

  1. const char * GetName ( ) const { return pName; }

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

  1. std::string & GetName ( ) { return name; }

Либо так

  1. const std::string & GetName ( ) const { return name; }

но никак иначе.

Это сделано специально, для работы с элементами класса через указатель, просто стоит знать что const для методов - возвращающих указатель на элемент - фактически не существует.

Комментарии

Аватар пользователя Илья Лесной

const

Очень хороший обзор: здесь

mutable

Хороший обзор здесь здесь

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

  1. #include <iostream>
  2. using namespace std;
  3.  
  4. class Mtest
  5. {
  6. mutable int x;
  7. const int y;
  8. public:
  9. Mtest( int _x=0, int _y=1 ): y(_y) { x=_x; }
  10. void SetX(int _x) const {x=_x;}
  11. int GetX() const { return x; }
  12. };
  13.  
  14.  
  15. int main ()
  16. {
  17. const Mtest Test1;
  18. Test1.SetX(1);
  19. cout << Test1.GetX() << endl;
  20. return 0;
  21. }

Замечания:
Константные переменные класса можно объявить только инициализаторами, происходит это потому что в стандарте языка Си++ заложено такое поведение, что если используется конструктор без инициализаторов, то при создании объекта его поля сначала будут инициализированы значением 0 (или конструкторами по умолчанию для пользовательских типов), потом им будет присвоено значение из оператора y=_y. Если же в классе использован конструктор с инициализатором, то при создании объекта его полям ("обработанных" инициализаторами) значения будут присвоены сразу. Поэтому, исходя из этого стандарта, становится понятным, почему поля константы, ссылки, и поля-объекты (без конструктора по-умолчанию) можно инициализировать только таким образом.

Аватар пользователя Илья Лесной

Заметил что среди источников в интернете даже нормальных - а не как этот сайт встречаются противоречия, неточности, и даже ошибки в учебных материалах.
Например ошибки в коде (скорее опечатки) есть тут http://www.amse.ru/courses/cpp2/2010_11_22.html. Но где их не бывает.
Хуже что в некоторых источниках сказаны неправильно основополагающие вещи -например что оператор разыменования не перегружается, возможно такая дезинформация пошла из-за неверного чтения в книжках оператора .* - это оператор выбора члена класса через указатель. Например
int ( monstr::*pget() ); - объявление указателя на методы с сигнатурой int somefunc ( ) {...};
pget = & monstr::get_health; -присваивание указателю метода класса
M - обычный объект, Md - динамический

(Md->*pget)( ); здесь работает операция доступа к члену динамического элемента - "селектор" (эта операция перегружаемая)
(M.*pget)(); здесь работает операция выбора члена через указатель на член (эта операция не перегружаемая)

Список какие можно а какие нельзя здесь

Стоит отметить о возможности синтаксической путаницы:

  1. //так записывается оператор разодресации
  2. double & operator * ( ) { return x; }
  3. //а так записывается оператор приведения типа
  4. operator double * ( ) { return _x; }

После опеределения операции приведения типа можем использовать такое:

  1. double * d = M;
  2. cout << (double*)(Md) << endl;

Cтоит заметить что операция приведения к одному и тому же типу может быть перегружена как для приведения к типу и приведения к указателю типа:

  1. template <class T, int kol> class Point
  2. {
  3. T * _x;
  4. public:
  5. //оператор разадресации
  6. T & operator * ( );
  7. //операторы приведения к типу
  8. operator T * ( );
  9. operator T ( );
  10. };
  11.  
  12. template <class T, int kol> T& Point<T,kol>::operator * ( )
  13. {
  14. return some_object;
  15. }
  16.  
  17. template <class T, int kol> Point<T,kol>::operator T * ( )
  18. {
  19. return _x;
  20. }
  21.  
  22. template <class T, int kol> Point<T,kol>::operator T ( )
  23. {
  24. return *_x;
  25. }

Тематика перегрузки оператора приведения типа с указателем - наталкивает на очень интересную тематику "умных указателей".

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

UPD
Оказывается и на Вики есть хорошая статья про то что перегружается и то что не перегружается.

Аватар пользователя Илья Лесной

удобно: (a<b)?a:b; вернет всегда меньшее. А (a>b)?a:b; большее.

Аватар пользователя Илья Лесной

Хотя конструкторы и деструкторы не могут ничего возвращать, синтаксис позволяет в них использовать оператор return - который ничего не делает. Стоит воспринимать как return из void ф-й (которые действительно ничего не возвращают, а не те, которые возвращают например нечто, которое далее может быть преобразовано во что-то - (char*)void_value - например.

Аватар пользователя Илья Лесной

Встретил описание что:

операция [ключевое слово, но будем говорить операция, чтобы не усложнять :) ] new вызывает конструктор по умолчанию для типов, которые она создаёт, как и операция new [ ]. Намомню - конструктор по-умолчанию для встроенных типов в Си++ - обращает все значащие биты переменной в нуль.

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

  1. // 1 2 3 4
  2. float * n = new float();
  3. // 1 - имя типа
  4. // 2 - имя указателя
  5. // 3 - операция new, которая вызывает:
  6. // 4 - конструктор типа float
  7. // выделяет память, необходимую для записи величины float
  8. // и присваевает её указателю

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

  1. // 5
  2. float * n = new float;

Но на самом деле здесь - undefined behavior. Например один и тот же код, собранный в linux и windows отработает по разному. В первой будет всё как описано выше, а во второй - указатель будет ссылаться на мусор.

В Си, после создания динамического объекта, принята практика ручной инициализации, т.к. си ф-ии выделения памяти не вызывают конструкторы по-умолчанию.

  1. memset(object, '\0', object_size);

для char * - '\0', а для чисел 0 или 0.0.

И эту практику, следует применять и в Си++, чтобы код был надёжным и переносимым. Для единичных переменных нужно применять явный вызов конструктора, т.е. вариант 4, а не 5, а для массивов использовать "алгоритмы" STL или ручной проход по массиву и инициализацию его элементов.

Кстати, в некоторых реализациях компилятора Си - malloc будет вести себя подобно new и new [] что для единичных переменных, что для массивов - тоже выполняет инициализацию нулями:

  1. int *dd = (int*)malloc (sizeof (int)); printf(" %d ", *dd);
  2. int *d = (int*)malloc(sizeof (int)*10);
  3. int i;
  4. for ( i=0; i<10; i++ ) printf(" %d ",d[i]);

Это касается gcc в linux, в windows (cygwin) в переменных будет случайный мусор из памяти.

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

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

Дело в том что для массивов конструирование по-умолчанию происходит только в первый раз. (кстати тот же вектор - который называют оберткой над стандартным массивом (причем так его обзывать очень несправедливо, но таки да, вектор внутри себя хранит данные в стандартном массиве) - лишен такой проблемы, и всякий новый раз после его полного очищения (std::vector<int>::clear()) и повторного выделения (std::vector<int>::resize(n)) будет происходить самая настоящая, принудительная инициализация элементов - конструкторами по умолчанию)

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

Например код ниже для си++ в linux (gcc) проинициализирует первое выделение нулями. А в windows (cygwin) нет.

При повторном выделении памяти, конструирование объектов не происходит. При этом поведение зависит от реализации компилятора и операционной системы - в linux (gcc) повторное выделение - не изменит карту ранее инициализированных данных, в windows (cygwin) "почти" не изменит - среди некоторых данных массива будет мусор.

Повторное перевыделение в Си++:

  1. int g;
  2. int *dd = 0;
  3. dd = new int[1024];
  4.  
  5. for (g=0; g<1024; g++) cout << dd[g] << ' ';
  6. cout << endl; // тут в выводе мы увидим нули (linux) или мусор (windows)
  7.  
  8. for (g=0; g<1024; g++) dd[g] = g+1;
  9.  
  10. for (g=0; g<1024; g++) cout << dd[g] << ' ';
  11. cout << endl; //а тут не нули)
  12.  
  13. delete [] dd; dd = 0;
  14.  
  15. dd = new int[1024];
  16.  
  17. for (g=0; g<1024; g++) cout << " " << dd[g] << " ";
  18. cout << endl; //и тут тоже не нули) (причем если windows то местами мусор)
  19.  
  20. int *d = new int;
  21. cout << "d=" << *d << endl; //тут нуль в linux, в windows мусор *1
  22. *d = 777; cout << "d=" << *d << endl; //тут везде 777
  23. delete d;
  24. d = new int;
  25. cout << "d=" << *d << endl; //а тут так же как и в *1

Повторное перевыделение в Си:

  1. int i;
  2. int * d = (int*) malloc (sizeof (int));
  3. *d = 13;
  4. free(d); d = NULL;
  5. d = (int*) malloc (sizeof (int));
  6.  
  7. printf(" %d\n ",*d); //ноль или мусор
  8.  
  9. int *dd = NULL;
  10. dd = (int*)malloc(sizeof (int)*1024);
  11.  
  12. for (i=0; i<1024; i++) printf(" %d ",dd[i]); // нули или мусор
  13. for (i=0; i<1024; i++) dd[i] = i;
  14. for (i=0; i<1024; i++) printf(" %d ",dd[i]); // не нули
  15.  
  16. free(dd);
  17. dd= NULL;
  18.  
  19. dd = (int*)malloc(sizeof (int)*1024);
  20.  
  21. for (i=0; i<1024; i++) printf(" %d ",dd[i]);
  22. //не нули, или не нули с мусором

Заключение

В общем случае лучше ориентироваться на то что - new выделяет память (и только) для размера переданного в него типа (который в блоке кода был помечен цифрой 5) При этом в зависимости от реализации выражение 5 будет воспринято либо просто как имя типа (и тогда new просто выделит память по размеру типа и не более, как в cygwin), либо 5 будет воспринято как 4 - т.е. не как имя типа а как вызов конструктора по-умолчанию. Поэтому для переносимости выделять память и инициализировать лучше с использованием скобок после имени типа (чтобы явно вызвать конструктор для простых типов).

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

А оператор new[] выделяет блок памяти, достаточный под размещения указанного количества объектов, при этом в начале блока помещается размер этого блока, и так же, после чего, вызываются конструктора по умолчанию объектов (но тут уже зависит от реализации).

А delete и delete[] в свою очередь вызывают деструкторы для объектов и далее освобождают выделенную память под объект или серию объектов (соотвественно).

Важное отличие new/delete от malloc/free - в том что с их помощью можно вызывать сразу конструктор создаваемого типа, malloс же выделяет только память, не конструируя объект.
new и new[] конструируют объект и на выходе будет объект нужного типа (механика процесса описана здесь). А в Си - возвращался тип непроинициализированный участок памяти - void*, который нужно было в ручную преобразовывать к нужному типу и далее, если это пользовательский тип - конструировать/уничтожать (если в нём внутри тоже есть динамика) в ручном режиме - что неудобно.

Кстати операция delete - защищена от удаления пустого указателя, но только в том случае, если указатель проинициализирован нулём или nullptr. В случае же неинициализированного указателя произойдет ошибка времени выполнения "double free or corruption". И её никак не предотвратить, кроме как проинициализировать указатель. if (pointer) или if (nullptr == pointer) - естественно не сработают, т.к. пока указатель не был инициализирован - в нём некоторый мусор, который "пройдет" эти проверки.

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

////upd 2020 прочитать как в стандарте нативные простейшие типы работают с new, скорее всего конструкторы по-умолчанию вызываются для составных типов, для простейших типов ничего не вызывается (предположение), иначе бы зачем был warning о неинициализированной переменной.

Аватар пользователя Илья Лесной

При отсутствии необходимости в обще используемой переменной - но нужде в некотором константном значении внутри класса - данное значение можно объявить не как static переменную а как элемент перечисляемого типа

  1. class CName {
  2. public:
  3. enum {
  4. sizeOfBuffer = 256
  5. };
  6.  
  7. char m_szFirst[sizeOfBuffer];
  8. char m_szLast[sizeOfBuffer];
  9.  
  10. public:
  11. void SetName(char* pszFirst, char* pszLast) {
  12. strcpy_s(m_szFirst, sizeOfBuffer, pszFirst);
  13. strcpy_s(m_szLast, sizeOfBuffer, pszLast);
  14. }
  15.  
  16. };
Аватар пользователя Илья Лесной

Так работать будет.

  1. for ( int i = n - 1; i >= 0; --i )
  2. {
  3. cout << "{" << i << "} ";
  4. }

а если заменить int на size_t ( unsigned int ) то работать не будет ( после =0 ), т.к. после декремента 0 (без знаковый тип) i получит значение UINT_MAX и получится бесконечный цикл. Правильное решение - в условии указать i > 0, а последний элемент выводить вне цикла.

Возможно однажды это будет причиной траты времени на поиск ошибки. Поэтому стоит упоминания.

UPD:
Просто на память про string::npos и size_t == unsigned_long и про то, почему npos == -1 (много где встречается). -1 - потому что это обратный код при приведении безнакового огромного типа до знакового int.

  1. cout << ( (typeid(string::npos) == typeid(unsigned long)) &&
  2. (typeid(size_t)) == (typeid(string::npos)) ) << '\n';
  3.  
  4. unsigned long d = ULONG_MAX;
  5.  
  6. //cout << '0' << ( d == 18446744073709551615 ) << endl //no ok
  7. // integer constant is so large that it is unsigned
  8. cout << "0) " << ( d == 18446744073709551615u ) << endl; //ok
  9. cout << "1) " << ( ( unsigned int ) d == 4294967295 ) << endl;
  10. cout << "2) " << ( string::npos == 4294967295 ) << endl;
  11. cout << "3) " << ( string::npos == ( signed int ) 4294967295 ) << endl;
  12. cout << "4) " << (string::npos == -1) << endl;
  1. 1
  2. 0) 1
  3. 1) 1
  4. 2) 0
  5. 3) 1
  6. 4) 1
Аватар пользователя Илья Лесной

Си:

Переменные объявленные внутри ф-ии с модификатором класса памяти static имеют постоянное время жизни с момента их первого объявления и (пока активна программа).

Переменные, и функции объявленные как static в глобальной области видимости - станут видны только в пределах модуля (в общем случае - файле) в котором они объявлены.

Си++:

Для переменных и ф-й унаследовал все из Си (экземпляры класса тоже можно ограничить областью видимости модуля, используя объявление static), но есть и своё:
статические переменные и методы класса - становятся общими для всех объектов класса. Статические методы могут работать только со статическими полями(переменными) класса (т.к. в них не передаётся скрытый указатель this). Время жизни статических полей - с момента их определения и до конца жизни программы - определение должно быть вынесено в глобальную зону видимости (вне всех классов и ф-й)

Аватар пользователя Илья Лесной

Для вывода контейнеров, вместо ручного их перебора (как например пришлось бы выводить массив в Си) очень удобно использовать функцию copy и итераторы. Причем если последний итератор /* в данном случае потоковый*/ (в который ведется копирование) соединить с объектов стандартного вывода - то код займёт всего одну строчку:

  1. copy( mySet.begin(), mySet.end(), ostream_iterator<int>(cout, " "));
  2. //на вход строка - поэтому именно двойные...

В конце, не стоит забывать о методах инициирующих очистку буфера (манипулятор endl например или метод fflush)

Аватар пользователя Илья Лесной

В Си и Си++ строки желательно делать нультерменированными.

Конструкция

  1. cout >> ch //где ch тип char *

создаст нультерменнированную си-строку, как и методы

  1. std::cin::get(buf, num, lim='\n');
  2. std::cin::getline(buf, num, lim='\n');

Так же нужно помнить что strlen считает только видимые символы без \0 - поэтому при аллокации нужно это учитывать:

  1. void assignWord(char ** target, const char * word)
  2. {
  3. if ( *target )
  4. {
  5. delete [ ] *target;
  6. *target = 0;
  7. }
  8.  
  9. //любая строка завершается \0 но strlen её не считает
  10. *target = new (std::nothrow) char [ strlen (word) + 1 ];
  11.  
  12.  
  13. if ( !(*target) )
  14. throw MemErr ( "Error mem alloc" );
  15.  
  16. memcpy ( *target, word, strlen(word) + 1 );
  17. //нуль терменирование необходимо для корректной работы
  18. //для работы с "Си строками" - иначе ф-ии strcpy, strlen будут выходить
  19. //за границы массива
  20. //и существует вероятность получения непредвиденного результата ("абракадабры")
  21. //причем даже в std::cout (тоже выводит си строки до \0 или до ошибки размера)
  22. }

Но конечно же вероятность абракадабры мала, и в случае если не увеличить размер на единицу в 10-й строке, то программист ничего и не увидит (до поры, до времени, пока иногда в работе программы не будут всплывать странные символы), а вот динамические анализаторы кода типа valgrind будут очень сильно ругаться. На то, что идет обращение за адресом массива. Но ведь всё работает. Почему так?

Любопытна некоторая механика массивов (Undefined Behavior)

Статический массив можно расширять - перешагивая за его размер (массив - последовательная структура данных, где каждый следующий элемент идёт за предыдущим, статический массив определен в стеке (слово статический в Си и Си++ несколько перегружено, здесь имеется в виду - тот, размер которого известен на этапе компиляции, физически же такой массив размещается на стеке и формально является автоматическим. Если же бы был объявлен вне ф-й, размещался бы в области данных), но расширять его можно, хотя информации об этом нет, т.к. это антипаттерн и Undefined Behavior. При этом расширять (как будет видно по коду ниже) можно и статический и динамический массив - но первый не вызовет варнингов, а второй вызовет - но в примере ниже оба отработают хорошо (выводя на экран то что нужно). Но так не стоит делать - прицеплять данные за границами массива - т.к. поведение не определено - и "незаконно" прицепленные данные могут быть уничтожены другим объектом (если это куча), в стеке же они защищены от затирания, т.к. добавление в стек только последовательное.

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

При этом, например на платформе linux (gcc) для статического массива из 2-х элементов, свободное место будет до 10-го элемента, а в windows (cygwin) до 3-го. Далее произойдет ошибка сегментирования. Но это на запись, читать же можно намного с большими индексами (видимо при чтении ошибка произойдет только при выходе за стек. А при записи она происходит, когда программа пытается записать данное в другую переменную, хранящуюся в стеке)

При желании потрогать такое "незаконное" добавление в массив - можно взять этот код, заодно удивить то, что printf для модификатора %c выводит строку вплоть до символа конца строки, или, если такого символа нет - то до исключительной ситуации, но которая, к счастью, не приводит к ошибки времени выполнения.

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

Если же взять статический-статический массив :) (т.е. такой который объявлен вне в-й), то такой массив располагается в области данных, если изначально проинициализирован, или же в области BSS, если известен его размер, но инициализация не произведена (BSS до передачи управления в main полностью инициализируется нулями). В итоге если массив разместился в BSS - то он автоматом всегда нультерменирован, если же в сегменте данных, то согласно теории он не должен являться нультерменированным, но по факту тоже таковым является на системе linux+gcc (в виндовс лень проверять). Так же добавлю что в отличии от массивов на стеке - к массивам в области данных и сегменте BSS можно добавлять намного больше элементов. По этой ссылке - наглядно где что располагается в плане сегмента данных/BSS. Так же в Linux есть удобная программа size, с помощью которой видно распределение по сегментам статических данных процесса.

Заключение

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

  • Любые строки в Си и Си++ объявленные канонично, т.е. объявленные например как строковая константа
    1. char name[20] = "Илья";

    автоматически (компилятором) завершаются нультерминантом ('\0').

  • Ф-ии чтения из стандартного потока как Сишные так и Плюсовые - завершают прочитанную строку '\0'.
  • Именно по "нулевому байту" '\0' ориентируется все другие ф-ии работающие на чтение со строками на тему конца строки. Такие как printf, strlen, std::cout + <char_type*> и так далее...
  • Если '\0' в строке не окажется - программа не упадёт (скорее всего это UB, но все компиляторы успешно обрабатывают такую ситуацию), просто читающие ф-ии аварийно прекратят чтение, а в результате их работы может появиться "абракадабра" что может привести к неправильному ходу вычислительного процесса или неккорекным данным - т.е. стоит внимательно относиться к таким случаям.
  • Обычно достаточно сложно получить строку не терминированную нулём, т.к. все стандартные ф-ии создания строк будут добавлять этот нуль '\0'. Но легко получить при работе с си-строками при копировании строки в новую строку, по размеру strlen (как в коде выше)
  • Стоит знать то что записываемые в файл строки будут содержать только видимую информацию никакого нультерминанта или символа конца файла в файл записано не будет.
  • При чтении из файла операционная система сама определяет конец файла и в таком случае посылает в поток символ конца файла - EOF.

UPDATE 02.09.2016 В этой заметке верно всё. Могу добавить ещё: на момент написания я не знал что массивы переменной длинны на стеке появились как в Си (и в Си++ - хотя в некоторых источниках утвреждают что в ++ их нет) по требованию научного сообщества. Но в книгах почти не описываются (в моих=). Отличительной особенностью такого массива - является удобство добавления элементов (может неограниченно расти, пока хватит стека, но есть и ряд ограничений, например он не может быть инициализирован при объявлении, а так же например таких. Хотя конечно же они не причем относительно изложенной особенности, просто к слову.

Аватар пользователя Илья Лесной

  1. #include <iostream>
  2.  
  3. using namespace std;
  4.  
  5. struct A {
  6. explicit A( int ) {}
  7. };
  8.  
  9. struct B {
  10. B( int ) {}
  11. };
  12.  
  13. int main() {
  14. A a(1); // ok
  15. B b(1); // ok
  16.  
  17. //A a2 = 1; // error: cannot convert from int to A
  18. // неявный вызов конструктора, и запрет его вызова
  19.  
  20. B b2 = 1; // ok
  21. // полный запрет вызова конструктора
  22. return 0;
  23. }

Полезный опыт и вообще зачем нужен explicit
Ссылка 1
Ссылка 2
Ссылка 3
Ссылка 4

Аватар пользователя Илья Лесной

Соотношение между обратным и прямым итератором

Пусть i - прямой итератор, тогда:

  1. iterator j = i;
  2. &*(reverse_iterator(i)) == &*(--j);

При этом .rend() указывает на элемент следующий за последним, но т.к. последовательность обратная - он указывает на элемент, следующий за первым. Но как такое возможно, ведь согласно стандарту в последовательностях (T*) гарантируется осмысленность значения указателя на элемент следующий за последним. По ссылке утверждается что значение указателя за первым элементом последовательности не определено, но фактически, для физически непрерывных последовательностей (т.е. тех, в которых все элементы физически следуют друг за другом - т.е. обыкновенном одномерном массиве) гарантируется осмысленность и указателя на элемент за "левой" границей непрерывной последовательности.

За счет этого свойства и достигается, указанное выше соотношение между прямым и обратным итератором.

Обратный итератор - является адаптером обыкновенного итератора (оберткой). Внутри себя он содержит поле c именем current, в котором хранится обычный итератор.

Например инкремент в обратном итераторе построен на декременте содержащегося в нём обыкновенного итератора.

  1. reverse_iterator&
  2. operator++()
  3. {
  4. --current;
  5. return *this;
  6. }

Конструктор же обратного итератора принимает на вход объект прямого итератора. И всё, т.е. никакой "дополнительной магии" не происходит. Всё просто.

Как это выглядит на самом деле:

  1. std::vector<int>::reverse_iterator rit_begin(std::vector<int>::iterator(vect.end()));
  2. std::vector<int>::reverse_iterator rit_end(std::vector<int>::iterator(vect.begin()));
  3.  
  4. std::deque<int>::reverse_iterator rdit_begin(std::deque<int>::iterator(deq.end()));
  5. std::deque<int>::reverse_iterator rdit_end(std::deque<int>::iterator(deq.begin()));

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

  1. std::vector<int>::reverse_iterator rit_begin(vect.rbegin());
  2. std::vector<int>::reverse_iterator rit_end(vect.rend());
  3.  
  4. std::deque<int>::reverse_iterator rdit_begin(deq.rbegin());
  5. std::deque<int>::reverse_iterator rdit_end(deq.rend());

Ну а далее работает соотношение

  1. iterator j = i;
  2. &*(reverse_iterator(i)) == &*(--j);

и гарантия осмысленности адресов элементов, за границами непрерывной последовательности.

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

Пример использования

  1. #include <iostream>
  2. #include <vector>
  3.  
  4. int main ()
  5. {
  6. std::vector<int> myvector (5); // 5 default-constructed ints
  7.  
  8. int i=0;
  9.  
  10. std::vector<int>::reverse_iterator rit = myvector.rbegin();
  11. for (; rit!= myvector.rend(); ++rit)
  12. *rit = ++i;
  13.  
  14. std::cout << "myvector contains:";
  15. for (std::vector<int>::iterator it = myvector.begin(); it != myvector.end(); ++it)
  16. std::cout << ' ' << *it;
  17. std::cout << '\n';
  18.  
  19. return 0;
  20. }
  21.  
  22. //output: myvector contains: 5 4 3 2 1

Интересное видео на тему:

и ссылка
http://en.cppreference.com/w/cpp/iterator/reverse_iterator

Аватар пользователя Илья Лесной

Просто на память как работает remofe_if для std::list. Так же впервые в этом блоге упомянается про c++11 :)

  1. #include <list>
  2. #include <iostream>
  3.  
  4. //variant one
  5. class IsMoreN
  6. {
  7. private:
  8. int _n;
  9. public:
  10. IsMoreN(int n = 10): _n(n) { };
  11. inline bool operator ( ) ( int n ) { return n > _n; }
  12. };
  13.  
  14. //variant two
  15. bool isMore10 ( int n ) { return n > 10; }
  16.  
  17.  
  18. int main()
  19. {
  20. int _l[] = { 1,100,2,3,10,1,11,-1,12 };
  21. std::list<int> l(_l, _l + sizeof(_l)/sizeof(_l[0]) );
  22.  
  23.  
  24. // remove all elements greater than 10
  25.  
  26. //c++11 - noname function ( lambda function )
  27. l.remove_if( [ ](int n){ return n > 10; } );
  28.  
  29. //classic c++
  30. l.remove_if ( IsMoreN( ) ); //call by functional object
  31. l.remove_if ( isMore10 ); //call by function
  32.  
  33. for (std::list<int>::iterator i = l.begin(); i != l.end(); ++ i)
  34. {
  35. std::cout << *i << ' ';
  36. }
  37. std::cout << '\n';
  38. }
  39. //output: 1 2 3 10 1 -1

Так же наглядно видно, где практически могут быть нужны лямбда ф-ии - там где их можно быстро написать и там где она пригодится лишь однажды.

Так же видно, где предпочтительнее функции чем функциональные объекты для алгоритмов и методов STL - например там, где достаточно постоянного условия сравнения. Но на самом деле в подобных вопросах - функции не предпочтительнее нигде - об этом ниже.

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

  1. std::list<T>::remove_if(Predicate pred);

если нужно удалить из списка элементы >2 то используем вызов

  1. l.remove_if ( IsMoreN(2) );

Если предикат должен являться обобщенной функцией (т.е. шаблонный предикат), то в алгоритмы std::list::remove_if и std::remove_if такую ф-ю можно передать только через непосредственное инстанцирование и взятие адреса данной инстанции:

  1. template <class T> bool some_predicate (T val) { return val < 3; }
  2. ...
  3. std::remove_if ( beg, end, &some_predicate<int> )

Или через адаптер указателя на функцию:

  1. #include <functional> // для адаптеров указателей на функции
  2. ...
  3. template <class T> bool some_predicate (T val) { return val < 3; }
  4. ...
  5. std::remove_if ( beg, end, std::ptr_fun(some_predicate<int>) )

Шаблонные классы-функторы такое ограничение не касается.

Использование ptr_fun в данном контексте излишне, т.к. конструирование объектов типа std::pointer_to_unary_function и std::pointer_to_binary_function несет за собой лишние расходы.

Нельзя не упомянуть о особенности алгоритма std::remove_if
В виду его реализации std::remove_if - на самом деле происходит лишь перемещение элементов, согласно предикату, а удаление не происходит. Поэтому, для настоящего удаления можно писать что-то вроде этого:

  1. int arr[]={1,2,3,4};
  2. std::vector<int> vct1 (arr,arr+4); // вектор есть последовательность 1 2 3 4
  3.  
  4. // std::remove_if ( vct1.begin(), vct1.end(), &some_predicate<int> );
  5. // сделает последовательность 3 4 3 4
  6.  
  7. vct1.resize( std::distance( vct1.begin(), vct1.end() =
  8. std::remove_if(vct1.begin(), vct1.end(), &some_predicate<int>)) );
  9. // будет 3 4

Чем функции предикаты хуже объектов предикатов или лямбда выражений

В "алгоритмах" STL выгоднее использовать функторы или лямбда функции (начиная с 14-го стандарта, лямбды так же могут быть типонезависимыми. T.к. функтор и лямбда - компилятором воспринимаются как тип, и встраиваются в создаваемый им код, поэтому операция вызова функции - () может быть определена как встроенная, а в случае предиката функции - её вызов будет осуществляться по обращению к указателю (накладные расходы на переход по другому адресу памяти и использование стека вызовов) - на выходе будет значительно медленнее работающая программа. Подробнее здесь.

Аватар пользователя Илья Лесной

Благодаря механизму шаблонов можно передать в ф-ю/метод на одно и то же место как объект так и указатель - используя это работают конструкторы в итераторной форме или методы .insert(iterator first, iterator last).

Например в ф-ю ниже можно передать как указатель так и просто объект

  1. template <class T> void foo(T x)
  2. {
  3.  
  4. if ( typeid(x) == typeid(int))
  5. {
  6. std::cout << "int\n";
  7. std::cout << x << std::endl;
  8. }
  9. if ( typeid(x) == typeid(int*))
  10. {
  11. std::cout << "int*\n";
  12. int * p = reinterpret_cast<int*>(x); std::cout << *p << std::endl;
  13. // cast for non pointer call foo(int). если бы не было вызова с обычным
  14. // параметром можно было бы скомпилять так
  15. // int * p = x; std::cout << *p << std::endl;
  16. }
  17. }
  18. //avaible call
  19. int * t = new int(1);
  20. int x = *t;
  21. foo(t);
  22. foo(x);

В шаблоне так же можно указать указатель в параметре (так обычно не пишут) т.к. не имеет особого смысла ( при передаче указателя в шаблон - компилятор сделает его указателем (как-бы уберет звездочку при инстанцировании), но зато в такой форме нельзя будет передать просто int т.к. на вход ожидается *. При этом будет такая ошибка компиляции no matching function for call to ‘foo(int&)’ - видно, что по-умолчанию компилятор передает в ф-ю элемент по ссылке.

  1. template <class T> void foo(T * x)
  2. {
  3.  
  4. if ( typeid(x) == typeid(int))
  5. {
  6. std::cout << "int\n";
  7. std::cout << x << std::endl;
  8. }
  9. if ( typeid(x) == typeid(int*))
  10. {
  11. std::cout << "int*\n";
  12. int * p = reinterpret_cast<int*>(x); std::cout << *p << std::endl;
  13. // cast for non pointer call foo(int). если бы не было вызова с обычным
  14. // параметром можно было бы скомпилять так
  15. // int * p = x; std::cout << *p << std::endl;
  16. }
  17. }
  18. //avaible call
  19. int * t = new int(1);
  20. int x = *t;
  21. foo(t);
  22. //foo(x);

Т.к. увидели, что компилятор в ф-ю передаёт элемент по ссылке - но ф-я не принимает его (Т x) - что будет если поставить (Т & x)?

  1. template <class T> void foo(T & x)
  2. {
  3.  
  4. if ( typeid(x) == typeid(int))
  5. {
  6. std::cout << "int\n";
  7. std::cout << x << std::endl;
  8. }
  9. if ( typeid(x) == typeid(int*))
  10. {
  11. std::cout << "int*\n";
  12. int * p = reinterpret_cast<int*>(x); std::cout << *p << std::endl;
  13. // cast for non pointer call foo(int). если бы не было вызова с обычным
  14. // параметром можно было бы скомпилять так
  15. // int * p = x; std::cout << *p << std::endl;
  16. }
  17. }
  18. //avaible call
  19. int * t = new int(1);
  20. int x = *t;
  21. foo(t);
  22. foo(x);

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

  1. template <class T> void foo(T & x)
  2. {
  3.  
  4. //x = new T; ERROR - нельзя выделить память под указатель
  5. //*x = new T; ERROR - нельзя разыменовать указатель
  6.  
  7. // а так можно
  8. T * n = new T;
  9. if (x == 0)
  10. x = *static_cast<T*>(n);
  11. *x = 12;
  12. }
  13. int * x = 0;
  14. std::cout << x << '\n';
  15. foo(x);
  16. std::cout << x << '\n';
  17. std::cout << *x << '\n';
  18.  
  19. //output:
  20. 0
  21. 0x19f40c0
  22. 12

Еще пример как работают шаблоны

Например есть массив int _l[] = { 1,100,2,3,10,1,11,-1,12 };

и ф-я, в которую он передается template <class T> T find ( T  ar );

такая запись более универсальна, т.к. возвращаемый тип автоматом становится указателем, как и входящий ar, так же, в неё можно уверенно кидать объекты ( не указатели ) но с поддержкой операций адресной арифметики и разодресации (как итераторы)

можно было бы записать так: template <class T> T * find ( T * ar ); - но в таком случае, как и говорилось выше мы не можем передать в ф-ю простой объект.

для более наглядного понимания шаблонов сделаем так

template <class T> T * find ( T  ar );

Тогда в аr войдет массив, и мы сможем внутри ф-ии с ним нормально работать используя *(ar+i/*int*/), но при возвращении значения произойдет такой косячек (времени компиляции)
cannot convert ‘int*’ to ‘int**’ in return - т.е. мы явно указали что возвращаем указатель, но компилятор T ar автоматически сделал T * ar и на выходе получили T ** .
Во всем этом можно убедиться используя внутни ф-ии

std::cout << typeid(ar).name() << "\n";

и за её пределами

std::cout << typeid(find(t)).name() << '\n';

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

template <class T> T * find ( T * ar, int n, T value)

Но если мы хотим чтобы кроме указателей передавались и объекты то придется писать так:

template <class T, class X > T find ( T ar, int n, const X & value)

Это конечно ненужные подробности, но немного проливающие факты реализации шаблонов.

Аватар пользователя Илья Лесной

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

Если задуматься, то согласно стандарту (си и си++) элемент следующий за последним - может быть определен лишь в непрерывной последовательности, аля char * c или int mas[N]. Вывод: в механически_не_последовательных контейнерах (да, std::list последовательный контейнер, но его элементы в памяти друг за другом не последовательны) - элемент, следующий за последним, вероятно должен существовать физически, а соответственно для него должна быть выделена и память. Проверим эту догадку.

Например что будет если "переитерировать влево" за границу первого элемента контейнера?

  1. std::list<int>::iterator it( L.end() );
  2. it--; // реверсные итераторы компактнее для обратного прохода
  3. do
  4. {
  5. std::cout << *(it--) << ' ';
  6.  
  7. } while (it != L.begin() );
  8. std::cout << std::endl;
  9.  
  10. std::cout << *(it) << std::endl;
  11. std::cout << *(it--) << std::endl;
  12. std::cout << *(it--) << std::endl;

сначала разыменование итератора выдаст 0 (хотя в контейнере 0 нет), а далее пойдет начиная с последнего элемента.

А если проитеитерировать за элемент следующий за последним?

  1. for (it = L.begin(); it!=L.end(); ++it )
  2. std::cout << *it << ' ';
  3. std::cout << std::endl;
  4.  
  5. std::cout << *(it) << std::endl;
  6. std::cout << *(++it) << std::endl;
  7. std::cout << *(++it) << std::endl;

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

Вывод

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

Это касается только контейнера list а так же других контейнеров, в которых физически элементы не следуют один за другим в оперативной памяти.

Кстати, благодаря такой особенности можно использовать std::copy () на list, для копирования большего контейнера в меньший, и это не приведет к ошибке, просто элементы меньшего контейнера будут перезаписываться. Это только для list. Например std::copy не сможет работать с std::set в качестве контейнера-приёмника, т.к. std::set<type>::iterator в Си++98 конвертируется в const (каким механизмом это достигается, я не нашел, но факт остаётся фактом методы begin и end имеют неконстантный тип итератора, но возвращают именно константный тип) а в c++11 вообще объявлен только const iterator (видимо для поддержки концепции множества (как отсортированного контейнера - для запрета писать в произвольное место элемент, т.к. это нарушит целостность сбалансированного дерева). То же касается и других ассоциативных контейнеров :)

Для контейнера же vector схожие действия по итерированию за .begin() и .end() приведут к рандомному результату (разыменование за границей - выдает рандомный результат, стабильно (за исключением маловероятных случаев что за границей уже нет памяти кучи), но конечно же профилировщики будут ругаться на обращение за адресом, аналогично разыменованию обычного указателя при "перескоке". А при работе с функцией std::copy( ), при выходе итератора за границы end() произойдет ошибка времени выполнения.

В исходниках stl::list итераторы вполне себе обычны - просто перепрыгивают в нужную сторону и всё. Вывод - в stl::list - в присутствует элемент объявленный как следующий за последним, но для него выделена память и он проинициализирован конструктором по-умолчанию своего типа, более того, соответствующие поля данного элемента списка указывают на предыдущий элемент (этим достигается типичная операция --list.end() ), а указатель на следующий элемент указывает на первый элемент последовательности. Так же, обратное поле первого элемента списка - указыват на элемент следующий за последним. Получается что список закольцован в памяти, а разделителем служит элемент следующий за последним.

В ассоциативных контейнерах - элемент следующий за последним, при разыменовании возвращает количество элементов в контейнере (начиная отсчет с нуля) При "итерировании за границу" итераторы ассоциативных контейнеров, ведут себя так же как итераторы std::list.

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

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

А вот при итерировании влево за границу первого элемента:

Итерируется нормально (каждый раз перескакивая на нужное количество байт внутри контейнера)

Если же разыменовать то при самой первой итерации за границы первого элемента - будет segfault - оно и не удивительно, учитывая внутреннюю кухню std:deque.

По этой причине нужно быть внимательными с деками при обращении к их элементу, следующему за первым, код ниже приведет к ошибке времени выполнения (для всех же остальных контейнеров, данный код был бы безопасен, ну разве что кроме ругани valgrind на то что берем адрес перед блоком выделенной памяти "Address 0x5a003fc is 4 bytes before a block of size 40 alloc'd" и то только для вектора)

  1. std::deque<int>::reverse_iterator rdit = deq.rend();
  2. std::cout << *rdit << ' ';

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

ох, еще стоит сказать что std::copy для дека не по размеру копируемого контейнера - поведет себя почти так же как и std::list, но скопируются только первые вошедшие (не будет замещения как для std::list или же segfault как для std::vector)

Аватар пользователя Илья Лесной

Утверждалось, что возможно только такое

  1. const std::string & GetName ( ) const { return name; }

И это так! Но возможно и такое.

  1. int & get_private_val() const {return *val_;}

И этот код корректно скомпилируется. И даже предоставит доступ к private.

Подробнее:

  1. class PrivateExit
  2. {
  3. private:
  4. int *val_;
  5. int &val2_;
  6. template <typename T> struct refToPointer { typedef T type; };
  7. template <typename T> struct refToPointer<T&> { typedef T* type;};
  8.  
  9. public:
  10. PrivateExit(int &val2, int val=0):
  11. val2_(val2), val_(new int(val))
  12. {}
  13.  
  14. ~PrivateExit(){ delete val_; delete typename refToPointer<int&>::type(&val2_); }
  15. int & get_private_val() const { return *val_; }
  16. };
  17. ...
  18. PrivateExit test(2);
  19. test.get_private_val() *= test.get_private_val();
  20. cout << test.get_private_val() << endl; //4
  21. ...
  22. ...
  23. typedef std::ostream_iterator<int> cIt;
  24. int arr[] = {1,2,3,4,5};
  25. // 1 2 3 4 5 -> 2 3 4 5 6
  26. // все это сработает и в 98-м, только нужно писать функтор
  27. // и передавать указатели на массив вручную.
  28. for_each ( std::begin(arr), std::end(arr),[](auto& v){++v;} );

&val2_ - для эксперимента, в котором видно, что хранить внешний объект внутри класса, можно не только в указателе, но и непосредственно по ссылке. Подход хранения объекта, переданного по-ссылке в указателе, используется например в std::ostream_iterator. Чтобы не было скучно - сделан финт ушами по очищению памяти объекта, который хранится по ссылке. Для используется ни что иное как статическое приведение (во время компиляции), ручной реализации :)

Так же здесь демонстрируется как что с помощью алгоритма for_each - можно менять последовательность, чисто на память, т.к. в источниках он перечисляется как алгоритм не меняющий последовательность. - но это имеется в виду сам контейнер (физически останется прежним, ни убавится, ни прибавится в нём число элементов) а вот сами элементы могут легко меняться, т.к. std::for_each в третий параметр передает ссылку.

  1. template<class InputIt, class UnaryFunction>
  2. UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f)
  3. {
  4. for (; first != last; ++first) {
  5. f(*first);
  6. //reference operator * ( )
  7. //reference is T& template type
  8. }
  9. return f;
  10. }
Аватар пользователя Илья Лесной

Вроде всё это знал, но не высыпаюсь и забыл (
https://msdn.microsoft.com/ru-ru/library/fxky5d0w.aspx

Аватар пользователя Илья Лесной

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

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

Решить такое можно только одним путём.

Допустим есть последовательность 1, 2, 3, 4, 3, 2, 1. Компьютер работает в двоичной с.с., иногда это преимущество можно использовать :) Эта последовательность в двоичном кода будет такая: 001, 010, 011, 100, 011, 010, 001. Видно что для анализа можно смотреть элементы последовательности по-разрядам, т.е. берем младшие разряды - 1, 0, 1, 0, 1, 0, 1 - видно что кол-во нолей не четно. Следовательно у непарного числа в младшем разряде есть 0. Продолжая такие рассуждения для остальных разрядов, получается ответ 100(2)->4(10).

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

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

В алгебре есть операция, называемая "сложение по модулю 2", конечно же я когда-то её знал из алгебры (и знаю такую операцию и из программирования) но, не знал (забыл) что исключающее или и есть сложение по модулю. Почему его так называют? Потому что исходя из определения данной операции - на её выходе помещаются все разряды двоичных чисел по размеру наибольшего из операндов. 1 0 ^ 1 == 1 0 + 1 (в двоичной системе счисления) а вот 1 0 ^ 1 0 != 10 + 10, но равно 10 + 10 - 100 == 00.

Очень хорошо показано на картинке с этого сайта

Рассматривая остальные свойста операции XOR
можно удивить a ^ 0 == a; a ^ a == 0; a ^ b == 0 => a==b; a ^ b == b ^ a; Так же видна и математическая ассоциативность (a ^ b ) ^ c == a ^ ( b ^ c );

Перечисленные выше свойства "исключающего или" позволяют решить данную задачу в пару строк, за один проход.

  1. template <typename T > typename std::iterator_traits<T>::value_type
  2. findNotPairNumber(T first, T last)
  3. {
  4. typename iterator_traits<T>::value_type ret = *first++;
  5. for ( ; first != last ; ++ first )
  6. ret ^= *first;
  7. return ret;
  8. }
  9. ...
  10. int a [] = { 5, 213, 71, 34, 67, 34, 5, 67, 71 };
  11. std::cout << findNotPairNumber ( a, a + sizeof(a)/sizeof(a[0]) ) << std::endl;
  12. // output - 213
Аватар пользователя Илья Лесной

http://www.martin-ueding.de/en/programming/qsort/index.html
В оригинале говорится что Си++ версии работают быстрее, и именно этого и ожидаешь, т.к. си версия qsort вынужденна по указателю обращаться к функции компаратору - а это расходы на переход по адресу и накрутку стека, а в Си++ функтор же встраивается в актуализированную версию алгоритма sort, поэтому переход по другому адресу не происходит, а работа со стеком идет как работа с локальными переменными внутри ф-ии.

И это так. В теории.

Другое дело конкретные реализации. Даже если оптимизировать код и статьи так:

  1. std::vector <MyComplex>::iterator s(v.begin()); // New
  2. std::vector <MyComplex>::iterator e(v.end()); // New
  3. for (int i = 0; i < 1000; i++) { // Eq
  4. //............. // Eq
  5. std::sort(s, e); // New
  6. //............. // Eq

(здесь через ввод дополнительных переменных для итераторов достигается оптимизация того что std::sort не вызывает каждый раз методы класса вектора begin() и end(), и соответственно они не конструируют итераторы каждый раз в цикле - это действительно полезная оптимизация.

Так же можно подумать что можно оптимизировать "range based for", заменив его на классических проход по последовательности, предполагая что range based for каждый раз читает размер контейнера, но на самом деле перечитывание размера не происходит, это некоторая статическая конструкция - компилятор вычисляет количество итераций в начале цикла и идет строго по ним (т.е. тут все уже оптимизировано).

Поэтому во время прохода изменять размер контейнера через range based for невозможно, ровно так же как и через итераторы. Т.к. при перераспределении памяти итераторы на вектор перестают быть валидными.

  1. for (auto & c:v)
  2. {
  3. if(c<5) { v.push_back(10);}
  4. std::cout << c << ' ';
  5. } // 0 0 3 4 5 6 7 8 9
  6.  
  7.  
  8. for (int i=0; i<v2.size(); ++i)
  9. {
  10. if(v2[i]<5) { v2.push_back(10);}
  11. std::cout << v2[i] << ' ';
  12. } // 1 2 3 4 5 6 7 8 9 10 10 10 10 10

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

Я отвлекся.

В общем - не смотря на то что в теории, в описанных сортировках, std::sort должен работать быстрее qsort - у меня была совсем другая картина, код который производил gcc и g++ - в Debian и Windows - показывал то что Си версия работала на порядок быстрее Си++. В FreeBSD же (clang и clang++) разница между Си и Си++ была почти незаметна, но все же Си версия тоже была быстрее. Интересно почему так.

Аватар пользователя Илья Лесной

В struct все поля по-умолчанию public - это единственное отличие.

Аватар пользователя Илья Лесной

Есть интересный шаблон проектирования, в котором конструкторы объявляются приватными, и есть статик ф-я, которая их вызывает, так можно делать в шаблоне "фабрика".

Аватар пользователя Илья Лесной

Хорошая статья про оценку сложности O().
Ещё хорошая статья
Двоичный поиск - это поиск на отсортированном массиве, известен так же как метод деления пополам или дихотомия.

Аватар пользователя Илья Лесной

В 11-м стандарте определены ф-ии, по действию аналогичные одноименным методам контейнеров. При использовании контейнеров - в них выгоды нет никакой, кроме того что код возможно (если есть using std) будет на один символ короче. Зато есть выгода при использовании статических стандартных массивов, в качестве контейнеров. А соотвтественно и возможность универсального кода:

  1. template <typename T> size_t cont_size(const T & c)
  2. {
  3. // if T is - some STD container - ok
  4.  
  5. // if T is - static array[] - ok, because size is part
  6. // of array type T == int[size]
  7.  
  8. // if dynamic array - std::begin and std::end - produce of
  9. // a compile error
  10. return std::distance(std::begin(c),std::end(c));
  11. }

Подробнее на stackoverflow.

Стоит внимания ф-я std::end для классических массивов - при передаче статических массивов в ф-ю обычным способом (под статическим подразумеваеются не те, что со словом static, а те, размер которых опеределен на этапе компиляции:

  1. // статический массив
  2. int arr[] = {1,2,3};
  3. // массив переменной длинны (не сатический)
  4. // автоматический, но выделяется во время выполнения на стеке.
  5. // такой невозможно передать в ф-ю std::end
  6. // хоть формально он и не отличается от обычного на уровне типов и т.д.
  7. // кстати такой массив так же нельзя передать в typeid:
  8. // error: typeid of array of runtime bound
  9. cin >> S; int arr[S];

так вот массивы, вне зависимости от указания или опускания размера в скобках, передаются в ф-ю как указатели - т.е. в ф-ю foo(int arr[3]) - можно будет передать любой размер массива, как это было бы foo(int * arr), но если же передать массив по ссылке (&a)[3] - то тут компилятор во первых примет только тот массив который нужен, а во вторых в ф-ии приёмнике такой массив будет как-будто-бы в области видимости своего определения - а это даёт нам возможность легко обращаться к его размеру (sizeof покажет размер массива а не указателя) - этим и пользуются авторы STL, правда размер они получают еще более быстро, чем это можно было бы через sizeof, которая тоже является операцией времени компиляции, но обращение к ней лишнее, т.к. здесь удобно использовать автовывод типов для ф-й шаблонов:

  1. template<class _Tp, size_t _Nm> inline _Tp* end(_Tp (&__arr)[_Nm])
  2. { return __arr + _Nm; }

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

Аватар пользователя Илья Лесной

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

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

  1. float * ptr = &vect[0];
  2. size_t size = vect.size();
  3. //обход и обращение к вектору не по итераторам а так:
  4. ptr[i]

Сделав так мы выиграем конечно не в многом - в стековой памяти, т.к. хранится только один указатель, и одна переменная размера (а соответственно и меньшее время конструирования), вместо классического подхода, который задействует два итератора (нововведенный "range based for" вероятно так же использует в свой работе указатель и константный размер, получаемый в момент компиляции)

В прочем чтобы не гадать на теории (не зная ассемблера, так особенно :) ) полезно применять тест стандартных контейнеров, который соритирует, потом удаляет дубликаты из последовательности. Спасибо Алёне.

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

Ну вот, теперь мы знаем на одну прикольную вещь больше, которая позволит оптимизировать наш код.

Без рассмотрения кода теста - может возникнуть некоторый дисонанс - как массив, вектор и дек, делают список - ведь речь идет о удалении. Если бы главный упор делался на удалении дубликатов - то да, список был бы быстрейшим, но в тесте всего один дубликат. В листе удаление одного элемента крайне быстрое, а в массиве, векторе, деке - очень не быстрое - требует перемещения остальных элементов... и таких смещений нужно сделать 1000/2 разной длинны... Отсюда видно - насколько эффективнее сортировка полследовательностей в которых элементы стоят друг за другом, а не рандомно. Т.е. контроллер памяти является самым слабым звеном в вычислениях (hdd в учет не берем).

Аватар пользователя Илья Лесной

В 98м си функции могли быть типонезависимыми только "сверху вниз" - это шаблоны. Думаю понятно о чём я. Типовые параметры шаблонов определяются извне для функции шаблона. Она только "подчиняется". Конструкция же -> decltype(type) своего рода "шаблон наоборот", (конечно это не шаблон, но похоже). Т.к. с помощью такой конструкции компилятор автоматически изменят тип возвращаемого значения. Это очень удобно - например в случае если значение берется из "глубоких недр" базового класса, где оно может поменяться, и в случае такой реализации оператора разыменования:

  1. auto operator*( ) const -> decltype ( (*_obj)[_iteration] )
  2. {
  3. return (*_obj)[_iteration];
  4. }

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

Приведённый метод разыменования приведён из, производного из базового класса, итератора. Конечно в данном случае, принято в самих итераторах хранить нужный тип в виде синонима на этот тип под именем ( данном случае ) reference. Так было заведено ещё с 98-го стандарта в STL, но, кто знает, если бы на момент разработки STL был автовывод типов, возможно некоторых полей, в контейнерах и итераторах не было бы :)

Для внимательных читателей, заметивших странное - индексация применяется к разыменованному объекту - да необычно, но так устроена внутренняя структура итератора, (автор не я), мне вообще сначала показалось, что лучше сделать ссылкой на контейнер _obj, (тогда работа внутри operator* выглядела бы более стандартно, плюс есть мнение что с ссылками получается более быстрый код, чем с указателями), но не буду вдаваться в такие подкапотные рассуждения, к тому же очень вероятно что использование указателя а не ссылки - связано с обеспечением возможности "переуказания" объекта итерартора совсем на другой контейнер того же типа - а тут уже очень выгодно хранить именно в указателе.

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

  1. auto operator*( ) const
  2. {
  3. return (*_obj)[_iteration];
  4. }

А вот описание мегауниверсальной функции:

  1. template<typename T, typename U>
  2. auto add(T t, U u) -> decltype(t + u);

Ведь T+U вполне себе может давать Q :)

i love ++ :)

Аватар пользователя Илья Лесной

Аватар пользователя Илья Лесной
Аватар пользователя Илья Лесной

Иногда в классе нужно использовать слово using таким образом.

  1. struct B : A
  2. {
  3. using A::f;
  4. void f(long);
  5. };

Зачем - информация здесь.

Аватар пользователя Илья Лесной

Интересное отличие Си++ от Си. (в лучшую сторону как по мне)
link one
link two

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

При этом, будет такой интересный момент:

  1. //------fun.c
  2. #include <stdio.h>
  3. float a=2;
  4.  
  5. int foo(){ printf("%d\n",a); a = -1; return a;}
  6.  
  7. //------main.c
  8. #include <stdio.h>
  9.  
  10. int a;
  11. // стоит понимать что у переменных не может быть объявления
  12. // т.е. это не объяление, а определение переменной, но без присвоения значения
  13. // а вот так extern a; - это было бы уже объявление переменной, объяленной где-то вне
  14. // данной единицы трансляции.
  15.  
  16. int foo();
  17. // а это именно объявление ф-ии, к объявлениям ф-й необязательно писать extern.
  18.  
  19. int main()
  20. {
  21. a = 1;
  22. printf("%d\n",foo());
  23. printf("%d\n",a);
  24. return 0;
  25. }
  26.  
  27. // вывод программы:
  28. -1387705416 // случайная величина из printf в foo
  29. // (каждый раз разная, т.к. скорее всего линковщик сслыется неизвестно куда,
  30. // особо не вникал)
  31. -1 // величина из присвоения в foo
  32. -1082130432 // постоянная величина, не вникал откуда берется.

Компилируя данный код из двух файлов, gcc -std=c99 -c *.c && gcc -o bin.bin *.o && rm *.o, мы получим то, что переменная из foo "перебивает" переменную из main. В C++ такое было бы невозможно, т.к. переменная с одним именем должна существовать в рамках компонуемой (из разных единиц трансляции) программы только в единственном экземпляре.

Аватар пользователя Илья Лесной

Есть статейка с хорошим описанием move семантики. Вот здесь. И да, по ней можно учиться. И не только move семантике но и анализу и критике кода.

Например здесь const String& operator=(const String& str) используется неправильный оператор уничтожения объекта.

В операторе перемещающего присваивания вообще не очищается память, так же неправильно используется конструктор переноса использующий std::move, и не совсем неправильно ведется описание самой ф-ии move. Посмотреть про move можно здесь. Он просто оборачивает объект в тип правосторонне-ссылочного. Всё. Не более того. Дальше работа уже за компилятором, который видя что объект является правосторонне ссылочным выберет для присвоение оператор перемещающего присваивания. (если же его нет, то копирующего)

Для корректной же работы конструтора в примере по статье не хватает инициализаторов (скорее всего автор их забыл в виду того что в его случае код работал (из-за отсутствия оператора delete[]). В общем вот такой патч к коду той статьи (все остальное же там верно и рекомендую к прочтению)

  1.  
  2. String(String&& str):m_pData(nullptr),m_size(0)
  3. {
  4. // устранение избыточного кода, задействованием
  5. // const String& operator=(String&& str)
  6. *this = std::move(str);
  7. }
  8. const String& operator=(String&& str)
  9. {
  10. if(this != &str)
  11. {
  12. delete [] m_pData;
  13. m_pData = str.m_pData;
  14. m_size = str.m_size;
  15. str.m_pData = nullptr;
  16. str.m_size = 0;
  17. }
  18. return *this;
  19. }

Часто хорошие описания есть у, как ни странно, но Microsoft.

На одном из онлайн курсов по Си++, приводился такой пример оператора =.

  1. const String& operator=(String&& str)
  2. {
  3. String copy(std::move(str));
  4. m_size = copy.m_size;
  5. // переносим динамическое данное в автоматический
  6. // объект что бы оно уничтожилось. Алгоритмичненько.
  7. std::swap(m_pData, copy.m_pData);
  8. return *this;
  9. }

Для такой формы нужна более развернутая реализация перемещающего конструктора (как первый вариант из статьи)

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

Важно

Вывод в консоль материалов из статьи по первой ссылке возможен только при компиляции с ключом -fno-elide-constructors, он запрещает компилятору GCC производить некоторые оптимизации(L_2.1_s13.mp4) связанные с тем что компилятор может устранять некоторые вызовы конструкторов (а соответственно и деструкторов) из процесса генерации кода, и передавать их на прямую. Но не стоит полагаться на такую оптимизацию, стоит писать грамотный код.

Аватар пользователя Илья Лесной

Новые спецификаторы в C++11 [зеркало], кроме этой ссылки так же рекомендую прочитать эту информацию.

Пример, аккумулирующий информацию с указанных выше ссылок

  1. #include <iostream>
  2. struct A
  3. {
  4. virtual void f(int) {std::cout << "FI" << std::endl;}
  5.  
  6. //без метода ниже f(long) при косвенной связи (указатель на базовый класс)
  7. //будет вызываться только базового класса, т.к. у производного она
  8. //не определена virtual void f(long) {std::cout << "NOOO" << std::endl;}
  9. };
  10. struct B : A
  11. {
  12. //без using метод f(long) перекрывает метод f(int) базового класса
  13. using A::f;
  14. void f(long) /*override*/ {std::cout << "FL" << std::endl;}
  15. };
  16.  
  17. int main(int argc, char *argv[])
  18. {
  19. int i = 1;
  20. long l = 1L;
  21. B b;
  22. b.f(i); // без using вызвалось бы B::f(long), а с usign B::A::f(int)
  23.  
  24. A * a = new B();
  25. a->f(l); //без перегруженной версии в базовом классе для long всегда
  26. //будет вызываться общий для всей иерархии метод f(int)
  27. }
Аватар пользователя Илья Лесной

хорошо описано здесь

Аватар пользователя Илья Лесной

Ускороение виртуальных методов

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

  1. B b;
  2. A *a = &b; // or A &a = b;
  3. a->A::foo(); // or a.A::foo();
  4. // B не содержится в А, поэтому для
  5. // раннего связывания можно поступить так:
  6. static_cast<B*>(a)->Bar();

Тело чисто виртуального метода

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

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

  1. struct A
  2. {
  3. virtual void foo() = 0;
  4. virtual ~A()=0;
  5. };
  6. A::~A(){}
  7. void A::foo(){}
  8. struct B:A
  9. {
  10. void foo() override
  11. // статический (не полиморфный) вызов
  12. // чисто виртуальной функции.
  13. { A::foo(); }
  14. };
  15. ...
  16. A a; // Compile error
  17. B b; // ok
Аватар пользователя Илья Лесной

Допустим мы пишем некоторый ассоциативный массив, в качестве его внутренних, так сказать "несущих контейнеров", мы собираемся использовать некоторый шаблон класса Array:

  1. template <class T> class Array { };

Тогда наш ассоциативный массив будет выглядеть так:

  1. template <class Key, class Value>
  2. class Map
  3. {
  4. ...
  5. Array<Key> key;
  6. Array<Value> value;
  7. };
  8.  
  9. // еще можно так, просто для удобства пользования "несущим
  10. // контейнером" внутри класса
  11. template <class Key, class Value>
  12. class Map
  13. {
  14. template <class T> using Container = Array<T>;
  15. ...
  16. Container<Key> key;
  17. Container<Value> value;
  18. };

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

  1. template <class Key, class Value, template <class> class Container = Array>
  2. class Map
  3. {
  4. Container<Key> key;
  5. Container<Value> value;
  6. };

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

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

  1. template <class T1, class T2, class Key = Array<T1>, class Value = Array<T2> >
  2. class Map2
  3. {
  4. ...
  5. Key key;
  6. Value value;
  7. };

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

  1. Map<short,long,Array<short>,std::vector<long>>() != Map<short,long,Array<short>,Array<long>>()

Это стоит учитывать, например при операциях сравнения объектов_контейнеров между собой. Могут сравниться лишь однотипные, хотя можно определить операции сравнения и над разнотипными, но стоит быть острожным при таком разнотиповом сравнении - подробнее здесь.

Аватар пользователя Илья Лесной

  1. std::vector <int> vct1{1,2,3};
  2. std::vector <int> vct2{4,5,6};
  3. std::swap(vct1, vct2);
  4. // or ?
  5. vct1.swap(vct2);

Обычно в реализациях STL алгоритм std::swap имеет перегрузки для всех стандартных контейнеров, основанные на работе с контейнерными swap методами. Контейнерные же swap методы в свою очередь обращаются к std::swap, (в векторе, например, меняются местами только "головы" внутренних "сырых" массивов, указатели на аллокаторы, а так же указатели на "элементы следующие за последним")

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

Причем начиная с 11 стандарта в

  1. template <class T> void swap (T& a, T& b)

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

В случае же использования алгоритма std::swap для обмена местами двух экземпляров пользовательского типа (естественно не обязательно контейнерного - а любого) можно добиться значительной производительности, если снабдить этот тип механизмом идеальной передаче.

Заключение
Запомним эту забавную архитектурную особенность: одна из перегрузок std::swap - образается к методу swap методу контейнера, который в свою очередь использует базовую версию алгоритма std::swap, для своей работы.

Аватар пользователя Илья Лесной

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

Случайно нашел, как с помощью метапрограммирования можно сделать шикарный "typecase" switch: сслыка.

Аватар пользователя Илья Лесной

Описание того, как правильно и как неправильно вызывают конструктор из конструктора.

Аватар пользователя Илья Лесной

Жуткий изврат, сам бы я таким не занимался никогда, как и вообще ни писал бы ничего на Си. Но одно дело то что хочется, а другое -то что приходится. И вот, когда нужно, то:
https://habrahabr.ru/post/205570/
https://habrahabr.ru/post/263547/

Аватар пользователя Илья Лесной

В то время когда в Си++ поддержка хешей есть на уровне языка, в Си приходится пользоваться этим
https://troydhanson.github.io/uthash/. Воротит от Си, но в данный момент приходится заниматься сервером, написанным на Си. А раньше было моим любимым языком, пока не познакомился с ++ =)

Аватар пользователя Илья Лесной

ссылка 1, сслыка 2, и еще на память.

Аватар пользователя Илья Лесной

Аватар пользователя Илья Лесной

https://habrahabr.ru/post/241941/

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

Объекто ориентированная модель конечного автомата на Qt http://doc.crossplatform.ru/qt/4.6.x/statemachine-api.html [my web archive link]
Пример реализации паралельных состояний https://ru.stackoverflow.com/questions/520422/Параллельные-состояния-в-qstatemachine [my web archive link]

http://cpp-code.ru/blog2/?q=node/74

Аватар пользователя Илья Лесной

замечания к комментарию - перечитывая определения pod из вики нашел:

Начиная с C++ 03, существует разница между записями T t; и T t();, а равно между new T и new T().

Версия с пустыми скобками называется "инициализация значением", а без них - "инициализация по умолчанию".

Инициализация по умолчанию: если конструктор умолчания тривиален, то не делается ничего, в объекте остается мусор. Если же конструктор умолчания нетривиален, то он исполняется.

Инициализация значением: если есть явно написанный конструктор умолчания, то он исполняется. Если же нет (т.е. если конструктор умолчания тривиален или же сгенерирован автоматически), то объект сначала зануляется, и только потом исполняется конструктор (если он нетривиален). Скалярные типы при инициализации значением зануляются.

Аватар пользователя Илья Лесной
Аватар пользователя Илья Лесной

Подборка хороших статей про Unicode
Прежде всего вот эта [my web archive link].
Далее эта [my web archive link].
Ну и конечно же соотвествующие страницы в Википедии.

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

Некоторое время назад, в одном из проектов было принято решение разработать свой класс, поддержки UTF-8 с определенным над ним всем множеством ф-й как в STL, и даже больше. Парочка методов один человек принес из джавы вроде-бы - по разбиванию строк на подстроки. Хотя я бы проектировал такое отдельной сущностью, ибо строка зависящая от вектора как-то архитектурно не красиво, имхо. Класс кстати был доделан и вроде как протестирован. Но проект в итоге так и не дописался (по причине что для всех, кроме, возможно автора проекта, это было хобби а не стартап). Я благорен ходу событий, за то что довелось поучаствовать в написании этого класса, там и аллокатор памяти, высокоэффективный, какой-то свой, вообще отличный от STL подхода аллокации в т.ч. и по интерфейсу, в связи с этим требующий внимательной работы с собой (ох, не мало времени ушло на отладку), свои итераторы - это самая продвинутая самостоятельная часть этого класса (аллокатор композитный член, поэтому его сложность я не учитываю), свои операторы сравнения, было и метапрограммирование, собственно "посравниваем несравнимое" это оттуда.

т.е. класс давал полноценную работу с Unicode, закодированном в UTF-8, т.е. возможность корректно итерироваться, корректно сравнивать посимвольно буквы, не важно какой они длинны, 1, 2, 3 или 4 байта. Т.е. то что не возможно стандартными средствами над UTF-8.

Кажется что куда удачнее было бы просто заюзать UTF-32, нативную для Linux бинарников - и была бы намного более быстрая итерация и сравнение между собой символов. Чем относительно сложные деревья функциональных вызовов и арифметических операций обрабатывающих неконстантный размер символа UTF-8. Памяти больше бы кушалось да. Впрочем предполагалось что клиент класса будет гонять очень большие наборы символов, и экономия в памяти была бы значительной, а т.к. предпологалось что клиент класса запускается на VDS то это возможно и повысило бы производительность в разы, т.к. исключало бы своппинг.

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

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

И вот у меня случился целый мини-ресерч, т.к. по началу я совершенно и предположить не мог, что строка, закодированная в UTF-8 (файл же в этой кодировке), например такая wchar_t *str = L"строка"; будет представлена совершенно не в UTF-8, хотя файл сохранён в этой кодировке. Я уже не помню, но видимо наивно ожидал увидить в wchar_t типах UTF-8 как-буд-то в char, а остальные байты, когда размер символа меньше размера wchar_t нулями заполнено или типа того. Да, тупанул тогда ощутимо, возможно был невыспан. Но зато был интересный опыт, и спуск прям до битового дебагинга, запросы к сообществу(1, 2), но в итоге приятное ощущение что разрулил, узнал, и сделал кросплатформенную работу с Unicode в проекте :)

UPD 24.03.2020

Примечательна ситуация в Windows, в которой вероятно из-за стремления совмещать с legacy решениями, и выпускать на рынок продукты в расчете не свои планы а не на мировые стандарты (поэтому в Windows есть ряд своих собственных обозначений кодировок, которые появлялись иногда чуть раньше чем были приняты в международных стандартах, а соответственно то что весь мир называет такой-то кодировкой, и что под этим имеет в виду Microsoft может иногда немного отличаться, ну в общем что-то такое можно найти в ссылках которые приведу ниже) - в Linux с кодировками все значительно лучше, повсеместный utf-8 в объектах хранения (имена файлов, контент файлов). Но кстати не стоит по таким конкретным примерам обобщать, есть вещи которые в Linux ощутимо хуже реализованы (или не реализованы вообще) чем в Windows.

Итак, я тут недавно возвратился опять к этому проекту (и как свойственно почти всем людям подзабыл детали, перечитывал) и обратил внимание что в проекте в win-версии чтобы прочитать имена файлов я декодирую их из кодировки windows-1251.

Ну думаю ок, если в Windows действительно вплоть до 2020го года в консоли по умолчанию для России - cp866 - то это норм. Но позже посмотрел на некоторые файлы в своём компе и там японские иероглифы и русские буквы рядом. И вот думаю как-так. Смотрю документацию из которой следует что в NTFS используется Unicode (в понимании Microsoft документации это UTF-16).

Я попытался найти конкретную информацию по такому преобразованию, но не нашел (когда писал код я подобрал кодировку при которой windows приложение работало с русскими именами правильно исходя из своих знаний о старых версиях windows, что русские буквы это window-1251, это кажется времена windows-xp были, когда n-я часть веб-сайтов так же кодировались в этой кодировке). Ну подобрал перекодировку из 1251 и заработало... А теперь, вспоминая код я не понимал почему...

В итоге находя информацию по крупицам (1, 2) можно сделать вывод 1) что существует два вида winapi ф-й (кстати часто в winapi какая-то ф-я это макрос к или A версии ф-ии или W версии, т.е. работающих соотвественно со стандартными строками (закодированными в OEM/Windows-xxxx) или же со строками закодированными в Unicode). Возможно можно было бы как-то указать этот момент в настройках проекта, вроде бы указывается в самом коде какой-то макрос и тогда идёт компиляция по "W сценарию". Но в документации от MS такое найти тяжело, вероятно потому что львиная часть документации расчитана как бы на применение в english реалиях, а там все просто как в Linux с его повсеместным UTF-8, сложности в Windows начинаются только при работе с локалями содержащими много отличных от латиницы символов - эти сложности кстати предваряют вывод 2) - что в Виндовс на данный момент работа с локалями через жопу и запутана (смотрим ссылку 2). Тем приятнее что во время разработки я относительно быстро справился с этим путём экспериментов и создал работающее приложение.

Ну и чтобы не забыть как оно работает (потому что вероятно(надеюсь, ибо интересно) - мне развивать и поддерживаеть его еще ближайший год а то и несколько) нарисую для себя принципиальную схему работы по отношению к кодировкам. Это рисунок про Windows версию. В Linux версии различия будут такие: в именах файлах, в кодировке в терминале применяется UTF-8, в приложениях UTF-32, потоки ввода/вывода можно даже не настраивать на кодировки потому что в них и так по умолчанию стоит локаль UTF-8, выполняющая преобразования на лету. Можно указать "ru_RU.UTF-8" но это не повлияет на буквенное отображение, а лишь может повлиять на представление дробных чисел в тексте или отображении времени в российском формате и т.п.

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

  1. // настройка потоков на кодировку терминала Windows
  2. void _setupSystemLocaleImpl() {
  3. setlocale(LC_ALL, getTerminalEncoding().first.c_str());
  4. auto myLocale = std::locale(getTerminalEncoding().first);
  5. const std::locale &l1 = std::wcout.imbue(myLocale); // for msvc compiller happy (C26444)
  6. const std::locale &l2 = std::wcin.imbue(myLocale);
  7. }

Имена же файлов приложение берёт используя FindFirstFile, причем как оказывается это макрос к FindFirstFileА, а есть ещё и FindFirstFileW:

  1. void _getFileListImpl(const std::string &dirPath, const std::string &extension,
  2. bool recursive, std::vector<std::string> &files) {
  3. ...
  4. std::string fnamePattern = dirPathNorm + "*";
  5. ...
  6. void* dir;
  7. WIN32_FIND_DATA entry;
  8. if ((dir = FindFirstFile(fnamePattern.c_str(), &entry)) != INVALID_HANDLE_VALUE) {
  9. do {
  10. if (!(entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
  11. std::string name(entry.cFileName);
  12.  
  13. if (name.find(_extension, name.size() - _extension.size())
  14. == name.size() - _extension.size()) {
  15. files.push_back(dirPathNorm + name);
  16. }
  17. }
  18. else if (recursive) {
  19. std::string dirName(entry.cFileName);
  20. if (dirName != ".." && dirName != ".") {
  21. _getFileListImpl(dirPathNorm + dirName + '/',
  22. extension, recursive, files);
  23. }
  24. }
  25. } while ((FindNextFile(dir, &entry)));
  26. ...

И вот, вероятно, т.к. в Windows вместо единого UTF-8 целый legacy зоопарк для поддержки локальных языков, (CP866, Windows-1251, UTF-16), то слой WinApi где-то неявно преобразует UTF-16 из имени файла в Win1251, это лишь догадка что именно этот слой, но кроме такой догадки я не вижу что еще может быть.

В итоге в самом приложении, мы можем открыть файл и работать с ним через через std::ifstream (думаю в реализации потоковых объектов от MS есть что-то что преобразует обратно локальную cp1251 в UTF-16 в именах перед обращением к файловой системе), а вот если попытаться вывести имя файла в консоль, не преобразовав его самим - то получим "абракадабру", поэтому используется ручное преобразование envToAppEncoding(fileName, fsCodePage_Win1251). После чего на поток ввода-вывода wcin (в проекте удобнее работать с широкими строками, поэтому опустим еще одно преобразование... и представим что выводится в cin), поступает строка в правильном внутреннем формате и он способен правильно "декодировать" в кодировку окружения. Примечательна ещё одна неразбериха - при работе с осязаемыми (то что мы выводим на консоль например) вещеми мы сами должны преобразовать, а вот слой WinAPI между приложением и прослойкой ФС делает такие преобразования неявно, что путает.

Дополнительно на рисунке показан дочерний процесс и коммуникация с ним на чтение и запись через каналы. Здесь работа ведётся так же через слой WinAPI, причем как с сырым потоком байтов, поэтому Си++ объекты стандартных потоков не могут перекодировать информацию, т.к. они тут не используются. Внешний дочерний процесс можно было бы настроить на передачу данных сразу в нужной кодировке, но это внешнее и закрытое приложение, разработаное для реалий российских версий MS Windows, а соотвественно читает в cp866 и отправляет в нём-же, поэтому подобно работе с файлами перед отправкой в канал мы должны сами преобразовать UTF16->CP866, а при чтении из потока сделать обратные действия.

Может всплыть вопрос - а если бы приложение выводило символы из всего набора Unicode в консоль? Ну в самой консоли Виндовс это было бы очень плохо. Есть способы перевести консоль в режим Unicode через настройки, но это все равно например не отображает в консоли Виндовс иероглифы, но по скольку поток байтов не связан с представлениями символов в консоле - то если известно что такое приложение отправляет в stdin сразу в определенной кодировке - то просто читать этот набор байт исходя из этого (в лучшем случае даже не преобразуя, а сразу получая от приложения нужную кодировку - UTF16).

Аватар пользователя Илья Лесной

Может быть опасной штукой:

  1. struct A
  2. {
  3. A(int a) : a_var(a) {}
  4. int a_var;
  5. };
  6.  
  7. struct B : public A
  8. {
  9. B(int a, int b) : A(a), b_var(b) {}
  10. int b_var;
  11. };
  12.  
  13. B &getB()
  14. {
  15. static B b(1, 2);
  16. return b;
  17. }
  18.  
  19. int main()
  20. {
  21. // Normal assignment by value to a
  22. A a(3); // a.a_var ==3
  23.  
  24. a = getB(); // a.a_var == 1, b.b_var not copied to a
  25.  
  26. B b2(3, 4); // b2.a_var==3 ,b2.b_var==4
  27.  
  28. A &a2 = b2; // Partial assignment by value through reference to b2
  29.  
  30. a2 = getB(); // b2.a_var == 1, b2.b_var == 4!
  31.  
  32. return 0;
  33. }

взято из вики [my web archive link].

Аватар пользователя Илья Лесной

https://habr.com/ru/post/130611/ [my web archive link 1]
http://softwaremaniacs.org/blog/2005/05/15/exceptions/ [my web archive link 2]

В дополнение:
Про noexpect [my web archive link 3].
Про разматывание стеке при исключениях и вызовы деструкторов [my web archive link 4].
Zero-cost Exceptions - cлучаи когда исключения ничего не стоят (и даже "дешевле", оптимальнее кодов возврата, если их не ловить, а если ловить то идентичны (в качестве подтверждения приведены листинги ассемблера)).

В дополнение (почти на эту тему)
Про грядущие контракты в C++20: https://habr.com/ru/post/443766/ [my web archive link 5]

Аватар пользователя Илья Лесной

Про механику переполнения косвенно в блоге уже упомяналось тут.

В интернете нашел одну интересную статью (текст там, имхо сотавлен тяжело, кажется что можно было, все что там написано, изложить удобочитаемее, чтобы понять пришлось прочитать 2,5 раза, и то не уверен что понял все корректно, но стоит иметь ввиду информацию оттуда) : https://www.viva64.com/ru/b/0589 [my web archive link 1]

Еще чуть про переполнения (в попытке понять почему переполнение безнаковых типов не считается неопределенным поведением. Полного понимания пока так и не добился): https://ru.stackoverflow.com/questions/513736 [my web archive link 2]

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

Аватар пользователя Илья Лесной

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