Гримуар C++

Nov 29, 2013 19:14

или N вещей, которые вы могли не видеть в C++ коде
или Как запугать жуниора

В коде на C++ можно увидеть много компиляторо-специфичных или просто редко встречающихся в учебниках штук, смысл которых туманен, а как их гуглить, иногда бывает неясно. К примеру, когда я, будучи еще школьником, переходил с бейсика на C++ и стремительно овладевал непростым понятием цикла for, поля класса и объявления переменных, мне встретился забористый кусок С++-кода с ::, namespace, template< template<> class >, что привело к поломке моего парсера :)


Самые обычные вещи

Сначала разминка

1. В списке объявления переменных астериски и амперсанды относятся к переменной, а не к типу.

int *a, *b, &c = *b;

2. В C++ довольно хитрые области видимости.

const int i = 1;
const int j = 2;
struct x { int x; };
namespace y {
    int i[i];
    int j = j;
    x x;
    int y = x.x;
};

3. Оператор стремления :)

int i = 10; while (i --> 0) cout << i << endl;
На самом деле, конечно, всего-лишь постдекремент и >.

4. В инварианте цикла можно объявлять переменные,

for (int i = 0; int j = 5 - i; ++i) cout << j << endl;
впрочем, как и в условиях.

if (C *c = get_ptr()) { }

5. А в инициализации можно объявлять типы.

for (struct { int a; float b; } i = {0, 1.0}; i.a < 5; ++i.a, i.b *= 3.14) {
    cout << i.a << " " << i.b << endl;
}

6. Всем известное устройство Даффа.

const int count = 12;
char dest[count], src[count] = "abracadabra", *from = src, *to = dest;
int n = 1 + (count - 1) / 8;
switch (count % 8) {
case 0: do { *to++ = *from++;
case 7:      *to++ = *from++;
case 6:      *to++ = *from++;
case 5:      *to++ = *from++;
case 4:      *to++ = *from++;
case 3:      *to++ = *from++;
case 2:      *to++ = *from++;
case 1:      *to++ = *from++;
} while (--n > 0);
}
printf("%s\n", dest);
Умные слова switch и case не должны скрывать от вас тот факт, что внутре у него goto.

7. В C++ есть несколько способов полностью обнулить структуру.

struct s { int a; float b; } s1, s2 = {0}, s3 = s();

8. Чтобы упростить копипасту в инициализации массивов, в конце может стоять запятая.

int arr[] = {2, 3, 5, 7, 11, };

9. Эта же запятая может стать источником проблем, если она перегружена. Если мы хотим этого избежать, используется такой трюк:

++i, void(), ++j
Например, в таком коде первый цикл исполняется с побочным эффектом:

struct commi
{
    int value;
    explicit commi(int value = 0) : value(value) { }
    commi& operator ++ () { ++value; return *this; }
    commi& operator -- () { --value; return *this; }
    bool operator < (const commi& rhs) const { return value < rhs.value; }
    void operator , (const commi& rhs) const { cout << "called " << value << "," << rhs.value << endl; }
};
ostream& operator << (ostream& out, const commi& rhs) { return out << rhs.value; }
template
void func(T arg_i, T arg_j)
{
    for (T i = arg_i, j = arg_j; i < j; ++i, --j) {
        cout << i << " " << j << endl;
    }
    for (T i = arg_i, j = arg_j; i < j; ++i, void(), --j) {
        cout << i << " " << j << endl;
    }
}

func(commi(), commi(5));

10. Тернарный оператор правоассоциативен и может выступать в качестве lvalue. Такой код сделает именно то, что должен, присвоит b = 5:

int a = 1, b = 2, c = 3, d = 4;
(a ? b : c) = d == 1 ? 2 : d == 2 ? 3 : d == 3 ? 4 : d == 4 ? 5 : d == 5 ? 6 : -1;
cout << b << c << d << endl;

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

cout << (42 ? 52 : throw logic_error("Why not 42?")) << endl;
42 ? work(42) : work(52);
42 ? void() : work(-1);
cout << typeid(1 ? new Derived : new Base).name() << endl;

12. Программисты на C++ часто забывают о битовых полях. А ведь их размеры могут задаваться аргументами шаблона. И еще у них есть поля-разделители (должны быть безымянными, выравнивают следующее поле по allocation unit boundary).

template struct bf {
    unsigned char a : X;
    unsigned char   : 0;
    unsigned char b : Y;
};
Я слышал, что у языка C++ есть какой-то безумный предшественник, называемый просто C. Вот, что они используют в ядре Linux вместо static_assert:

#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))
А еще вот такое.

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

struct A { virtual ~A() = 0; };
A::~A() { cout << "~A" << endl; }
struct B : A { ~B() { cout << "~B" << endl; } };

14. Многоточие и макросы для взаимодействия с ним:

void func(int n, ...)
{
    va_list args;
    va_start(args, n);
    int value;
    for (int i = 0; i < n; ++i) {
        value = va_arg(args, int);
        cout << value << endl;
    }
    va_end(args);
}

func(3, 123, 345, 456);

15. Function-try-block.

struct A {
    int a;
    A(int a) try : a(a > 0 ? a : throw 42) {
        cout << "I'm inside!" << endl;
    } catch (...) {
        cout << "Oops" << endl;
    }
};

16. Диграфы и триграфы, еще.

// What will the next line do? Increment???????????/
++x;

??=include
int main()
??<
    printf("Hello, world\n");
??>

int main(){ ; } // smile with beard!
В частности, из-за диграфов в Boost всегда пишут < :: вместо

17. Булевы операторы можно записать словами.

if (1 not_eq 2 and (0 or 1)) {
    cout << "WAT" << endl;
}
Пристально смотрите на слова and, and_eq, not, not_eq, or, or_eq, xor, xor_eq, bitand, bitor, compl.

Visual C++ требует для их работы

#include

18. Иногда компилятору нужно словами объяснить, что он перед собой видит. Ключевыми словами operator, typename и template.

struct A { template T operator () () const { return T(42); } };
struct B { template T operator () () const { return T(13); } };
struct C : A, B { };

struct Info {
    typedef A base_t;
    template struct info { typedef int type; };
};

template void func()
{
    cout << C().T::base_t::template operator()::type>() << endl;
}

int main()
{
    func();
}
Кстати, Visual Studio 2013 не переносит тут слова template перед operator, а GCC и Clang наоборот, его отсутствия. ++vc_bugs_discovered_by_me;

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

char *(*(**foo[][8])())[];


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

int               // integer
int *             // pointer to integer
int *[3]          // array of 3 pointers to integer
int (*)[3]        // pointer to array of 3 integers
int *()           // function having no parameters, returning pointer to integer
int (*)(double)   // pointer to function of double, returning an integer

//The equivalent SPECS type IDs are:
int                // integer
^ int              // pointer to integer
[3] ^ int          // array of 3 pointers to integer
^ [3] int          // pointer to array of 3 integers
(void -> ^int)     // function having no parameters, returning pointer to integer
^ (double -> int)  // pointer to function taking a double, returning an integer

20. Спецификация исключений. Самая, пожалуй, редко используемая возможность C++ (привет, Яндекс! :)), позволяющая указывать, что может выкинуть функция, карая нарушителей std::unexpected и std::terminate. Была беспощадно выпилена в C++11 в пользу более узкого noexcept.

void func1() throw(double) { throw 42.0; } // OK
void func2() noexcept { throw 42; } // not OK

21. Насколько извращенным может быть обычный new? В хорошем коде на C++ он почти не встречается вовсе, так что вы вполне могли не видеть чего-то вроде

char buff[sizeof(int) * 4];
cout << *new (buff) int[4]() << endl;
cout << *new (nothrow) int[4]() << endl;
Первый пример это placement new[] c new-initializer, создающий обнуленный массив из 4 интов на месте буфера buff. Второй обычный new[], но мы просим его не кидать исключений. Об этом и многом другом читайте 5.3.4 [expr.new] :)

22. Думаете, такой код не скомпилируется? Выдаст строку?

cout << 'FOUR' << endl;
На самом деле он выведет 1179604306 (на моей машине). Да, это ROUF, если смотреть побайтово и multicharacter literal, если смотреть в Стандарте. Удобно для всяких магических чисел типа FourCC.

23. А еще среди тяжкого наследия есть такая функция printf.

int a = 3;
float b = 3.14159265358979323f;
int c = 3735929054;
int d = 195948557;
int chars = 0;

printf("%.*f\n", a, b);
printf("%#-20x %#-20x %n %p\n", c, d, &chars, &chars);
printf("%08d\n", chars);
printf("%2$d %1$d", 1, 2);

3.142
0xdeadc0de           0xbadf00d             001AFAF4
00000042
2 1
Впрочем, последнее не работает нигде, кроме богомерзких юниксов, а заботливая Visual Studio отключает по умолчанию возможность использовать %n. Чтобы его включить, используйте

_set_printf_count_output(1);
Но если вам все же не хватило магии, загляните в документацию Boost.Format.

24. Параметром шаблона может быть функциональный тип.

function fn = less();

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

template
int array_size(T (&arr)[N]) { return N; }

int arr[42];
cout << array_size(arr) << endl;
Сишники же пишут

#define array_size(arr) (sizeof(arr) / sizeof(*arr))
Фу.

Темная магия C++11

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

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

struct A
{
    auto operator () (int x) -> function
    {
        return [this, x](int y) -> decltype(op(x, y)) {
            return op(x, y);
        };
    }

virtual int op(int x, int y) { return x + y; }
};

struct B : A
{
    int op(int x, int y) { return x * y; }
};

void init(function& fn)
{
    B b;
    cout << &b << endl;
    fn = b(3);
    //cout << fn(5) << endl;
}

void use(function& fn)
{
    A a;
    cout << &a << endl;
    cout << fn(5) << endl;
}

int main()
{
    function fn;
    init(fn);
    use(fn);
}
Такой код в релизе выводит 8, а если переключить комментарии при fn(5), то 15. Мы захватываем в возвращаемой из operator () лямбде this, указывающий на объект типа B, созданный на стеке функции init. Когда мы выходим из нее, он разрушается, но при правильной фазе Луны после захода в функцию use на том же месте создается объект типа A, который ничем не отличается от B, кроме значения vTable. Ничего не подозревающая лямбда вызывает через сохраненный указатель на this метод A::op.

27. Еще одна солянка из auto type inference, initializer list, uniform initialization, range-based for loops, non-static member initialization и right angle bracket problem.

template struct point {
    T x{0}, y{0};
    point(initializer_list il)
        : x{*il.begin()}, y{*(il.begin() + 1)} { }
};

vector
int>> points{{1, 2}, {2, 3}, {3, 1}};
for (const auto& pt : points) {
    cout << pt.x << " " << pt.y << endl;
}

cout << max({4, 3, 5}) << endl;

for (auto i : {1, 1, 2, 3, 5, 8, 13}) {
    cout << i << endl;
}

28. Еще одна большая возможность C++11 это variadic templates, резко упростившие работу со структурами вроде tuple.

namespace aux {
    templatesize_t...> struct seq { };

templatesize_t N, std::size_t... Is>
    struct gen_seq : gen_seq1, N-1, Is...> { };
    templatesize_t... Is>
    struct gen_seq<0, Is...> : seq { };

template
    void print_tuple(ostream& os, const Tuple& t, seq)
    {
        auto res = {0, (void(os << (Is == 0 ? "" : ", ") << std::get(t)), 0)...};
    }
} // namespace aux

template
ostream& operator << (ostream& os, const std::tuple& t)
{
    os << "(";
    aux::print_tuple(os, t, aux::gen_seq());
    return os << ")";
}

cout << make_tuple("abc", 123, 3.14) << endl;

Темная магия препроцессора

29. Простые вещи.

#if 0
    #if defined(BOOST_MSVC)
        #pragma message("warning")
    #else
        #warning "warning"
    #endif
    #error "You loose. Again."
#endif
#if 0 это такие вложенные комментарии для бедных; используйте всюду defined вместо #ifdef; генерируйте внятные сообщения об ошибках и предупреждениях.

30. С помощью препроцессора можно получить массу разной информации разной степени полезности.

/*
I'm just a function f or void __cdecl f(void) in file Source.cpp at line 29
in code compiled on Nov 29 2013 at 11:07:13 with counter = 0
*/
void f()
{
    cout << "I'm just a function " << __FUNCTION__ << " or " <<
#if defined(BOOST_MSVC)
        __FUNCSIG__
#else
        __func__ << " or " <<
        __PRETTY_FUNCTION__
#endif
        << " in file " << __FILE__ << " at line " << __LINE__
        << " in code compiled on " << __DATE__ << " at " << __TIME__
        << " with counter = " << __COUNTER__ << endl;
}

31. Содержимое макросов часто обрамляется циклом с ложным постусловием

#define F12(x) do { f1(x); f2(x); } while (0)
Нужно это для того, чтобы можно было писать вызов макроса, как будто это функция.

if (cond)
    F12(x);
else
    F12(y);
Фанаты 1TBS смотрят на любителей опустить фигурного скобца нахмуренно, свирепо и в то же время недоуменно.

32. Все знают, что делает первый макрос, но мало кто слышал про второй. Еще бы, ведь это специфичная возможность VC++.

#define makestring(x) #x
#define makechar(x) #@x
cout << makestring(My favorite char is) << " " << makechar(w) << endl;

Темная магия компиляторов

33. На TopCoder до недавнего перехода на gcc 4.8.1 -std=c++11 популярностью пользовались и >?=. Теперь про них написано в документации, что The G++ minimum and maximum operators (‘?’) and their compound forms (‘?=’) have been deprecated and are now removed from G++.

Из той же песочницы код

n ?: m; // равносильно n ? n : m

34. GCC поддерживает литералы вида

cout << 0x1.fp3 << endl; // 15.5
cout << 0b00101010 << endl; // 42
Откуда 15.5? 0x1.F это 1 + 15/16, а p3 задает экспоненту. Получается 1.11112 × 23 = 1111.12 = 15.5. А бинарные литералы войдут в C++14.

35. Среди адских GCC extensions есть и такой, разрешающий отрезки значений в switch.

switch ('w') {
case 'a' ... 'z':
    printf("lower");
    break;
case 'A' ... 'Z':
    printf("upper");
    break;
}

36. А еще в GCC есть свои лямбды, с БШ (statement exprs).

int x = 42;
int y = ({
    int res;
    if (x % 2 == 0) {
        res = x / 2;
    } else {
        res = 3 * x + 1;
    }
    res;
});

37. Вы все еще пишете

#if !defined(ONCE_H_SOME_LONG_HASH_JUST_IN_CASE)
    #define ONCE_H_SOME_LONG_HASH_JUST_IN_CASE
    // Код
#endif
include guards? Тогда мы идем к вам!

// once.h
#pragma once
void f() { cout << "Inside!" << endl; }

// main.cpp
#include "once.h"
#include "once.h"
Поддерживается и GCC, и VC++. Если нет разницы, зачем писать больше?

38. Аналогично и GCC, и VC++ поддерживает упаковку структур, полезную, например, при разборе бинарных форматов.

#pragma pack(push, 1)
struct SomeData {
    char c;
    int i;
    double f;
};
#pragma pack(pop)

cout << "size: " << sizeof(SomeData) << endl;
cout << "c offset: " << offsetof(SomeData, c) << endl;
cout << "i offset: " << offsetof(SomeData, i) << endl;
cout << "f offset: " << offsetof(SomeData, f) << endl;
/*
size: 13
c offset: 0
i offset: 1
f offset: 5
*/

39. А вот такой очень полезной прагмы в GCC нету.

#pragma comment (lib, "opencv_core246.lib")
Возможность указать в одном файле исходников нужную библиотеку и не плодить разные файлы проектов, cmake, autotools и прочего настолько удобна, что гнутые багописцы решили сделать корректность линковки зависящей от порядка перечисления библиотек, только бы ее не имплементить.

40. Существует магическая прагма (работающая как на GCC, так и на VC), которая волшебным образом иногда ускоряет ваш код.

vector sum(10);
#pragma omp parallel for
for (int i = 2; i < 10; ++i) {
    for (int j = i; j < 1000000000; j += i) {
        sum[i] += j;
    }
    cout << sum[i] << endl;
}
Степень колдунства сильно зависит от вашего знания OpenMP, задачи и количества ядер, а еще от того, не забыли ли вы его включить (в VC Project Properties > C/C++ > Language > Open MP Support, в GCC ключ -fopenmp).

41. Компиляторы предоставляют большое число различных атрибутов (MSVC, GCC), например, выдающих предупреждения при использовании устаревших функций/переменных/классов

#if defined(BOOST_MSVC)
#define DEPRECATED(func, msg) __declspec(deprecated(msg)) func
#else
#define DEPRECATED(func, msg) __attribute__ ((deprecated(msg))) func
#endif

int DEPRECATED(get42, "Use new_get42 instead")() { return 42; }
int new_get42() { return 6 * 7; }
VC умеет еще так

#pragma deprecated(get42)
Чтобы как-то побороть этот зоопарк, C++11 ввел специальный синтаксис для атрибутов, но пока не включил в требования к реализациям список всего полезного. Выглядеть это будет как-то так

int get42() [[deprecated("message")]] { return 42; }

42. В Windows SDK часто встречаются структуры вида

typedef struct _MIB_IFTABLE {
    DWORD dwNumEntries;
    MIB_IFROW table[ANY_SIZE];
} MIB_IFTABLE, *PMIB_IFTABLE;
где ANY_SIZE определен как 1. Но и VC, и GCC поддерживают массивы нулевого размера. Такой код

struct with_zero_arr {
    int a;
    int b[0];
};
скомпилируется (в VC с предупреждением) и будет работать, пока кто-нибудь не захочет создать объект этой структуры (а не только указатель).

43. Ключевое слово restrict есть и в C++. Пример из википедии:

void add(int *__restrict a, int *__restrict b, int *__restrict c)
{
    *a += *c;
    *b += *c;
}
Оно позволяет сообщить компилятору, что указатели указывают на непересекающиеся области памяти, а значит после изменения памяти по одному указателю ему не нужно будет перечитывать память по другим.

44. А еще на C++ можно писать как на PHP. Вот такой код

int $x = 123;
прекрасно компилируется и VC, и GCC.

45. В Visual C++ есть настоящие свойства.

class S {
    int i;

public:
    void setProperty(int value) {
        cout << "set" << endl;
        i = value;
    }
    int getProperty() {
        cout << "get" << endl;
        return i;
    }
    __declspec(property(get = getProperty, put = setProperty)) int property;
};

S s;
s.property = 5;
cout << s.property << endl;
В GCC их придется эмулировать.

46. Когда Даже если вдруг все накроется, программы на Visual C++ продолжат работать, потому что они умеют взаимодействовать с SEH!

int filter(unsigned int code, struct _EXCEPTION_POINTERS *ep)
{
    if (code == EXCEPTION_ACCESS_VIOLATION) {
        cout << "access violation" << endl;
        return EXCEPTION_EXECUTE_HANDLER;
    } else {
        cout << "unknown exception " << code << endl;
        return EXCEPTION_CONTINUE_SEARCH;
    }
}

int main()
{
    int* p = 0x00000000;
    __try {
        __try {
            *p = 13;
            /*
            access violation
            exception
            exception handler
            */
        } __finally {
            puts(AbnormalTermination() ? "exception" : "");
        }
    } __except (filter(GetExceptionCode(), GetExceptionInformation())) {
        cout << "exception handler" << endl;
    }
}
Если вы не используете какой-нибудь breakpad, всегда полезно оборачивать ваш код в __try и try с выводом какой-нибудь диагностики.

47. А вот так, внученько, мы в Visual C++ итерировались по массивам до этих ваших новомодных auto, range-based for loop и initializer-list.

int vec[] = {1, 1, 2, 3, 5, 8};
for each (int x in vec) {
    cout << x << endl;
}

48. Вообще называть переменные ключевыми словами нельзя, но в Visual C++, если очень хочется, то можно:

int __identifier(template) = 0;
++__identifier(template);
cout << __identifier(template) << endl;
Бывает полезно при конфликте имен с Microsoft specific keywords или при взаимодействии с кодом на C#, где template вполне может быть идентификатором.

Темная магия других языков

Иногда исходники на C++ перемешаны с другими, похожими языками.

49. Вот, например, хитрые сишники, у которых нет конструкторов и std::initializer_list понапридумывали кучу разных способов инициализации, например compound literals и designated inits. А [0 ... 5] это и вовсе GCC extension, работает с -std=gnu90.

struct player { char *name; int rank; } players[] = {
    [0].name = "foo",
    [0 ... 5].rank = 1, [6 ... 8].rank = 2,
    [9] = ((struct player) {"ninth", 9})
};

50. А когда сишники свободны от отлаживания утечек памяти, они завидуют Фортрану. Вот, например, продукт этой зависти, воплощенный в один из GCC extension:

int n;
    scanf("%d", &n);
    void *addr[] = {&&one, &&two, &&three};
    goto *addr[n];
one:
    printf("one\n");
two:
    printf("two\n");
three:
    printf("three\n");

51. Если вы увидите что-то жуткое вида

@interface MyObj : NSObject
+ (id) method1;
- (id) method2;
@end
MyObj *array = [[MyObj funcWith:arg and:arg2] init];
не пугайтесь, это всего лишь Objective-C, а сразу удаляйте.

52. Аналогично поступайте в случае кода вида

MyObj^ obj = gcnew MyObj;
Это C++.NET.

Ну и надо рассказать одной строкой пару страшилок про C++14. Появится yield, литералы вида 2+3i, 7h + 35min и 100'500, функции, принимающие и возвращающие auto, шаблоны переменных. Страустрап пилит паттерн-матчинг круче, чем в Скале. А потом появятся async/await, import, рефлексия времени компиляции и мир окончательно разделится на людей, которые успели выучить C++, пока он еще был простым, и на тех, кто никогда не осилит: первые будут писать весь полезный софт, а вторые клепать сайты-визитки.

Страшилки для других языков.

А какие плюсострашилки знаете вы?

программирование

Previous post Next post
Up