Язык C++ сильно изменился за последние 10 лет. Изменились даже базовые типы: struct, union и enum. Сегодня мы кратко пройдёмся по всем изменениям от C++11 до C++17, заглянем в C++20 и в конце составим список правил хорошего стиля.


Зачем нужен тип struct


Тип struct — фундаментальный. Согласно C++ Code Guidelines, struct лучше использовать для хранения значений, не связанных инвариантом. Яркие примеры — RGBA-цвет, вектора из 2, 3, 4 элементов или информация о книге (название, количество страниц, автор, год издания и т.п.).


Правило C.2: Use class if the class has an invariant; use struct if the data members can vary independently

struct BookStats
{
    std::string title;
    std::vector<std::string> authors;
    std::vector<std::string> tags;
    unsigned pageCount = 0;
    unsigned publishingYear = 0;
};

Он похож на class, но есть два мелких различия:


  • по умолчанию в struct действует видимость public, а в class — private
  • по умолчанию struct наследует члены базовых структур/классов как публичные члены, а class — как приватные члены

// поле data публичное
struct Base
{
    std::string data;
};

// Base унаследован так, как будто бы написано `: public Base`
struct Derived : Base
{
};

Согласно C++ Core Guidelines, struct хорошо применять для сокращения числа параметров функции. Этот приём рефакторинга известен как "parameter object".


Правило C.1: Organize related data into structures (structs or classes)

Кроме того, структуры могут сделать код более лаконичным. Например, в 2D и 3D графике удобнее считать в 2-х и 3-х компонентных векторах, чем в числах. Ниже показан код, использующий библиотеку GLM (OpenGL Mathematics)


// Преобразует полярные координаты в декартовы
// См. https://en.wikipedia.org/wiki/Polar_coordinate_system
glm::vec2 euclidean(float radius, float angle)
{
    return { radius * cos(angle), radius * sin(angle) };
}

// Функция делит круг на треугольники,
//  возвращает массив с вершинами треугольников.
std::vector<VertexP2C4> TesselateCircle(float radius, const glm::vec2& center, IColorGenerator& colorGen)
{
    assert(radius > 0);

    // Круг аппроксимируется с помощью треугольников.
    // Внешняя сторона каждого треугольника имеет длину 2.
    constexpr float step = 2;
    // Число треугольников равно длине окружности, делённой на шаг по окружности.
    const auto pointCount = static_cast<unsigned>(radius * 2 * M_PI / step);

    // Вычисляем точки-разделители на окружности.
    std::vector<glm::vec2> points(pointCount);
    for (unsigned pi = 0; pi < pointCount; ++pi)
    {
        const auto angleRadians = static_cast<float>(2.f * M_PI * pi / pointCount);
        points[pi] = center + euclidean(radius, angleRadians);
    }

    return TesselateConvexByCenter(center, points, colorGen);
}

Эволюция struct


В C++11 появилась инициализация полей при объявлении.


struct BookStats
{
    std::string title;
    std::vector<std::string> authors;
    std::vector<std::string> tags;
    unsigned pageCount = 0;
    unsigned publishingYear = 0;
};

Ранее для таких целей приходилось писать свой конструктор:


// ! устаревший стиль !
struct BookStats
{
    BookStats() : pageCount(0), publishingYear(0) {}

    std::string title;
    std::vector<std::string> authors;
    std::vector<std::string> tags;
    unsigned pageCount;
    unsigned publishingYear;
};

Вместе с инициализацией при объявлении пришла проблема: мы не можем использовать литерал структуры, если она использует инициализацию полей при объявлении:


// C++11, C++14: будет ошибка компиляции из-за инициализаторов pageCount и publishingYear
// C++17: компиляция проходит
const auto book = BookStats{
    u8"Незнайка на Луне",
    { u8"Николай Носов" },
    { u8"детская", u8"фантастика" },
    576,
    1965
};

В C++11 и C++14 это решалось вручную написанием конструктора с boilerplate кодом. В C++17 ничего дописывать не надо — стандарт явно разрешает агрегатную инициализацию для структур с инициализаторами полей.


В примере написаны конструкторы, необходимые только в C++11 и C++14:


struct BookStats
{
    // ! устаревший стиль!
    BookStats() = default;

    // ! устаревший стиль!
    BookStats(
        std::string title,
        std::vector<std::string> authors,
        std::vector<std::string> tags,
        unsigned pageCount,
        unsigned publishingYear)
        : title(std::move(title))
        , authors(std::move(authors))
        , tags(std::move(authors)) // ;)
        , pageCount(pageCount)
        , publishingYear(publishingYear)
    {
    }

    std::string title;
    std::vector<std::string> authors;
    std::vector<std::string> tags;
    unsigned pageCount = 0;
    unsigned publishingYear = 0;
};

В C++20 агрегатная инициализация обещает стать ещё лучше! Чтобы понять проблему, взгляните на пример ниже и назовите каждое из пяти инициализируемых полей. Не перепутан ли порядок инициализации? Что если кто-то в ходе рефакторинга поменяет местами поля в объявлении структуры?


const auto book = BookStats{
    u8"Незнайка на Луне",
    { u8"Николай Носов" },
    { u8"детская", u8"фантастика" },
    1965,
    576
};

В C11 появилась удобная возможность указать имена полей при инициализации структуры. Эту возможность обещают включить в C++20 под названием "назначенный инициализатор" ("designated initializer"). Подробнее об этом в статье Дорога к С++20.


// Должно скомпилироваться в C++20
const auto book = BookStats{
    .title = u8"Незнайка на Луне",
    .authors = { u8"Николай Носов" },
    .tags = { u8"детская", u8"фантастика" },
    .publishingYear = 1965,
    .pageCount = 576
};

В C++17 появился structured binding, также известный как "декомпозиция при
объявлении". Этот механизм работает со структурами, с std::pair и std::tuple и дополняет агрегатную инициализацию.


// композиция структуры
const auto book = BookStats{
    u8"Незнайка на Луне",
    { u8"Николай Носов" },
    { u8"детская", u8"фантастика" },
    576,
    1965
};
// декомпозиция структуры
const auto [title, authors, tags, pagesCount, publishingYear] = book;

В сочетании с классами STL эта фишка может сделать код элегантнее:


#include <string>
#include <map>
#include <cassert>
#include <iostream>

int main()
{
    std::map<std::string, int> map = {
        { "hello", 1 },
        { "world", 2 },
        { "it's",  3 },
        { "me", 4 },
    };
    // пример №1 - разложение пары [iterator, bool]
    auto [helloIt, helloInserted] = map.insert_or_assign("hello", 5);
    auto [goodbyeIt, goodbyeInserted] = map.insert_or_assign("goodbye", 6);
    assert(helloInserted == false);
    assert(goodbyeInserted == true);

    // пример №2 - разложение пары [key, value]
    for (auto&& [ key, value ] : map)
        std::cout << "key=" << key << " value=" << value << '\n';
}

Зачем нужен тип union


Вообще-то в C++17 он не нужен в повседневном коде. C++ Core Guidelines предлагают строить код по принципу статической типобезопасности, что позволяет компилятору выдать ошибку при откровенно некорректной обработке данных. Используйте std::variant как безопасную замену union.


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


// ! этот код ужасно устрарел !
// Event имет три поля: type, mouse, keyboard
// Поля mouse и keyboard лежат в одной области памяти
struct Event
{
    enum EventType {
        MOUSE_PRESS,
        MOUSE_RELEASE,
        KEYBOARD_PRESS,
        KEYBOARD_RELEASE,
    };

    struct MouseEvent {
        unsigned x;
        unsigned y;
    };

    struct KeyboardEvent {
        unsigned scancode;
        unsigned virtualKey;
    };

    EventType type;
    union {
        MouseEvent mouse;
        KeyboardEvent keyboard;
    };
};

Эволюция union


В C++11 вы можете складывать в union типы данных, имеющие собственные конструкторы. Вы можете объявить свой констуктор union. Однако, наличие конструктора ещё не означает корректную инициализацию: в примере ниже поле типа std::string забито нулями и вполне может быть невалидным сразу после конструирования union (на деле это зависит от реализации STL).


// ! этот код ужасно устрарел !
union U
{
   unsigned a = 0;
   std::string b;

   U() { std::memset(this, 0, sizeof(U)); }
};

// нельзя так писать - поле b может не являться корректной пустой строкой
U u;
u.b = "my value";

В C++17 код мог бы выглядеть иначе, используя variant. Внутри variant использует небезопасные конструкции, которые мало чем отличаются от union, но этот опасный код скрыт внутри сверхнадёжной, хорошо отлаженной и протестированной STL.


#include <variant>

struct MouseEvent {
    unsigned x = 0;
    unsigned y = 0;
};

struct KeyboardEvent {
    unsigned scancode = 0;
    unsigned virtualKey = 0;
};

using Event = std::variant<
    MouseEvent,
    KeyboardEvent>;

Зачем нужен тип enum


Тип enum хорошо использовать везде, где есть состояния. Увы, многие программисты не видят состояний в логике программы и не догадываются применить enum.


Ниже пример кода, где вместо enum используют логически связанные булевы поля. Как думаете, будет ли класс работать корректно, если m_threadShutdown окажется равным true, а m_threadInitialized — false?


// ! плохой стиль !
class ThreadWorker
{
public:
    // ...

private:
    bool m_threadInitialized = false;
    bool m_threadShutdown = false;
};

Мало того что здесь не используется atomic, который скорее всего нужен в классе с названием Thread*, но и булевы поля можно заменить на enum.


class ThreadWorker
{
public:
    // ...

private:
    enum class State
    {
        NotStarted,
        Working,
        Shutdown
    };

    // С макросом ATOMIC_VAR_INIT вы корректно проинициализируете atomic на всех платформах.
    // Менять состояние надо через compare_and_exchange_strong!
    std::atomic<State> = ATOMIC_VAR_INIT(State::NotStarted);
};

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


// ! плохой стиль !
void FillSlide(unsigned slideNo)
{
    switch (slideNo)
    {
        case 1:
            setTitle("...");
            setPictureAt(...);
            setTextAt(...);
            break;
        case 2:
            setTitle("...");
            setPictureAt(...);
            setTextAt(...);
            break;
        // ...
    }
}

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


enum SlideId
{
    Slide1 = 1,
    Slide2,
    Slide3,
    Slide4
};

Иногда enum используют как набор флагов. Это порождает не очень наглядный код:


// ! этот код - сомнительный !
enum TextFormatFlags
{
    TFO_ALIGN_CENTER = 1 << 0,
    TFO_ITALIC = 1 << 1,
    TFO_BOLD = 1 << 2,
};

unsigned flags = TFO_ALIGN_CENTER;
if (useBold)
{
    flags = flags | TFO_BOLD;
}
if (alignLeft)
{
    flag = flags & ~TFO_ALIGN_CENTER;
}
const bool isBoldCentered = (flags & TFO_BOLD) && (flags & TFO_ALIGN_CENTER);

Возможно, вам лучше использовать std::bitset:


enum TextFormatBit
{
    TextFormatAlignCenter = 0,
    TextFormatItalic,
    TextFormatBold,

    // Значение последней константы равно числу элементов,
    //  поскольку первый элемент равен 0, и без явно
    //  указанного значения константа на 1 больше предыдущей.
    TextFormatCount
};

std::bitset<TextFormatCount> flags;
flags.set(TextFormatAlignCenter, true);
if (useBold)
{
    flags.set(TextFormatBold, true);
}
if (alignLeft)
{
    flags.set(TextFormatAlignCenter, false);
}
const bool isBoldCentered = flags.test(TextFormatBold) || flags.test(TextFormatAlignCenter);

Иногда программисты записывают константы в виде макросов. Такие макросы легко заменить на enum или constexpr.


Правило Enum.1: предпочитайте макросам перечислимые типы

// ! плохой стиль - даже в C99 этого уже не требуется !
#define RED   0xFF0000
#define GREEN 0x00FF00
#define BLUE  0x0000FF
#define CYAN  0x00FFFF

// стиль, совместимый с C99, но имена констант слишком короткие
enum ColorId : unsigned
{
    RED = 0xFF0000,
    GREEN = 0x00FF00,
    BLUE = 0x0000FF,
    CYAN = 0x00FFFF,
};

// стиль Modern C++
enum class WebColorRGB
{
    Red = 0xFF0000,
    Green = 0x00FF00,
    Blue = 0x0000FF,
    Cyan = 0x00FFFF,
};

Эволюция enum


В С++11 появился scoped enum, он же enum class или enum struct. Такая модификация enum решает две проблемы:


  • область видимости констант enum class — это сам enum class, т.е. снаружи вместо Enum e = EnumValue1 вам придётся писать Enum e = Enum::Value1, что гораздо нагляднее
  • enum конвертируется в целое число без ограничений, а в enum class для этого потребуется static cast: const auto value = static_cast<unsigned>(Enum::Value1)

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


enum class Flags : unsigned
{
    // ...
};

В некоторых новых языках, таких как Swift или Rust, тип enum по умолчанию является строгим в преобразованиях типов, а константы вложены в область видимости типа enum. Кроме того, поля enum могут нести дополнительные данные, как в примере ниже


// enum в языке Swift
enum Barcode {
    // вместе с константой upc хранятся 4 поля типа Int
    case upc(Int, Int, Int, Int)
    // вместе с константой qrCode хранится поле типа String
    case qrCode(String)
}

Такой enum эквивалентен типу std::variant, вошедшему в C++ в стандарте C++ 2017. Таким образом, std::variant заменяет enum в поле структуры и класса, если этот enum по сути обозначает состояние. Вы получаете гарантированное соблюдение инварианта хранимых данных без дополнительных усилий и проверок. Пример:


struct AnonymousAccount
{
};

struct UserAccount
{
    std::string nickname;
    std::string email;
    std::string password;
};

struct OAuthAccount
{
    std::string nickname;
    std::string openId;
};

using Account = std::variant<AnonymousAccount, UserAccount, OAuthAccount>;

Правила хорошего стиля


Подведём итоги в виде списка правил:


  • C.1: организуйте логически связанные данные в структуры или классы
  • C.2: используйте class если данные связаны инвариантом; используйте struct если данные могут изменяться независимо
  • используйте декомпозицию при объявлении переменных со структурами, std::pair и std::tuple: auto [a, b, c] = std::tuple(32, "hello"s, 13.9)
    • вместо out-параметров возвращайте из функции структуру или кортеж
  • указывайте инициализаторы полей, без них вы получите неинициализированные поля с мусором
    • не инициализируйте поля нулями в конструкторах, полагайтесь на инициализаторы полей
  • в общем случае не пишите конструкторы структур, используйте агрегатную инициализацию
  • используйте std::variant как безопасную замену union вместо структуры или класса, если данные находятся строго в одном из нескольких состояний, и в некоторых состояниях некоторые поля теряют смысл
  • используйте enum class или std::variant для представления внутреннего состояния объектов
    • предпочитайте std::variant, если в разных состояниях класс способен хранить разные поля данных
  • используйте enum class вместо enum в большинстве случаев
    • используйте старый enum если вам крайне важна неявная конвертация enum в целое число
    • используйте enum class или enum вместо магических чисел
    • используйте enum class, enum или constexpr вместо макросов-констант

Из таких мелочей строится красота и лаконичность кода в телах функций. Лаконичные функции легко рецензировать на Code Review и легко сопровождать. Из них строятся хорошие классы, а затем и хорошие программные модули. В итоге программисты становятся счастливыми, на их лицах расцветают улыбки.

Комментарии (63)


  1. kovserg
    06.08.2017 20:02
    +10

    Лучше покажите как строятся «хорошие программные модули» на современном C++


  1. Antervis
    06.08.2017 20:29
    -19

    методичка к третьей лабораторной работе первокурсников по информатике?


  1. NYM
    07.08.2017 00:06

    Можно к правилам хорошего тона добавить еще пожелание использовать в проде фишки стандарта, которого еще нет с осторожностью, не однократно подумав. Они к моменту релиза стандарта, могут пропасть из него…


    1. ZaMaZaN4iK
      07.08.2017 15:42
      +3

      Стандарт ещё на прошлом заседании был одобрен и уже давно отправлен в ISO. Так что боятся нечего насчёт С++17. Сейчас вся работа ведётся над C++20.


  1. IGHOR
    07.08.2017 02:17
    +6

    Зачем нужен тип enum

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


    1. invzbl3
      07.08.2017 09:15

      Благодарю за добротную статью. Очень пригодится в полноценном видении языка!


  1. SinsI
    07.08.2017 09:16
    +1

    Насколько «литеральная инициализация» вообще востребована?
    Ведь более-менее серъёзные программы обычно разделяют данные и код, загружая всё текстовое из внешних ресурсов…


    1. sergey_shambir Автор
      07.08.2017 09:39
      +2

      С помощью литеральной инициализации можно собрать несколько значений, возвращённых функцией, в структуру и избежать out-параметров. То есть цель не в том, чтобы хардкодить данные. Цель в лёгкой трансляции, композиции и декомпозиции данных. В плане декомпозиции очень помогает structural binding, про который я забыл в статье. Так выглядит декомпозиция структуры (хотя 5 переменных за раз конечно перебор):


      // композиция структуры
      const auto book = BookStats{
          u8"Незнайка на Луне",
          { u8"Николай Носов" },
          { u8"детская", u8"фантастика" },
          576,
          1965
      };
      // декомпозиция структуры
      const auto [title, authors, tags, pagesCount, publishingYear] = book;


      1. sergey_shambir Автор
        07.08.2017 09:57

        P.S. добавил в статью примеры structured binding


    1. DistortNeo
      07.08.2017 12:53
      +1

      Лично я вообще не понял, что автор имеет в виду под термином "литеральная инициализация". Судя по всему, к строкам и тексту это вообще не имеет никакого отношения, и "литеральной инициализацией" автор обозвал "aggregate initialization". Если это так, то нужно поправить публикацию.


      1. sergey_shambir Автор
        07.08.2017 13:12

        Согласен, переименовал под термин "агрегатная инициализация". Спасибо!


  1. Artygreed13
    07.08.2017 09:39
    -1

    До сих пор не понимаю зачем две одинаковые конструкции class и struct, уже давно нужно было выпилить struct. Никакой «локаничности» его использования нет.


    1. RdSpclL
      07.08.2017 15:19
      +1

      а обратная совместимость есть\


  1. StingerFG
    07.08.2017 09:40
    -3

    На древнем С++ можно запрограммировать Матрицу. Эти нововведения мешают кодерам, разве что полезное — инит переменных, всё.


    1. DarkEld3r
      08.08.2017 17:47

      Кому и чем мешают нововведения?


  1. sashagil
    07.08.2017 11:07

    Сергей, допустим, я описал тип struct или class с полем int, и я поленился написать конструкторы, а также ин-плейс инициализацию для этого поля. Я создаю инстанс моего типа (неявно используя дефолтный конструктор без параметров). Что говорит стандарт разных годов выпуска про значение int-поля этого инстанса?


    1. sergey_shambir Автор
      07.08.2017 11:21

      Стандарты всех версий считают, что неинициализированное нестатическое поле остаётся в неопределённом состоянии до первого присваивания. Чтение из такого поля — неопределённое поведение. Между тем, в некоторых компиляторах такие поля инициализируются нулями в отладочной версии и не инициализируются после включения оптимизаций. Кроме того, вероятность получить в виде мусора тот же 0 достаточно высока.


      Итог: ошибка проявляет себя не всегда, её трудно распознать. Проблему надо предотвращать в корне инициализацией при объявлении.


      • если в проекте используется анализатор C++ Core Guidelines, он должен сообщать о такой ошибке
      • если в проекте используют компилятор Clang, имеет смысл включить undefined behavior sanitizer в отладочных сборках


      1. kovserg
        07.08.2017 12:55

        А что должен вернуть такой код на разных редакциях C++

        #include <stdio.h>
        #include <stdlib.h>
        #include <string.h>
        
        void operator delete(void* ptr) { free(ptr); }
        void* operator new(size_t sz) {
            void *dst=malloc(sz);
            if (dst) memset(dst,0xFF,sz);
            return dst;
        }
        
        struct A { int x; };
        
        struct B : A { A a; int y; };
        
        int main(int argc,char** argv) {
            A *a=new A();
            B *b=new B();
        
            printf("a->x=%d\n",a->x);
            printf("b->x=%d\n",b->x);
            printf("b->y=%d\n",b->y);
            printf("b->a.x=%d\n",b->a.x);
        
            delete b;
            delete a;
        }
        


        1. DistortNeo
          07.08.2017 13:34

          Поменяйте строчки


              A *a=new A();
              B *b=new B();

          на


              A *a=new A;
              B *b=new B;

          И почувствуйте разницу между no initialization и default initialization.


          1. kovserg
            07.08.2017 13:55

            Почувствовал



      1. sashagil
        08.08.2017 04:45

        Спасибо! Понятно, что «проблему надо предотвращать в корне», я хотел разобраться, что говорит текущий стандарт (и как давно он так говорит). Если проблема не предотвращена в корне, ситуация становится вот какой (внизу в комментариях некоторые аспекты обсуждаются) — если для нового инстанса класса с дефолтным конструктором (и без ин-плейс инициализации POD-полей) не указана явно "()-инициализация" (например, написано ' new S; ', а не ' new S(); '), инстанс не инициализирован, в нём мусор. Однако, если "()-инициализация" указана (как в упомянутом ' new S(); ' или при вызове std::make_unique), текущий стандарт всё-таки диктует инициализацию нулями. Эта последняя деталь упоминается в некоторых обсуждениях на StackOverflow, например, но я не нашёл чёткую отсылку к стандарту. Полез смотреть сам (https://github.com/cplusplus/draft/blob/master/papers/n4687.pdf), нашёл вроде бы соответствующий пункт на странице 226:

        if T is a (possibly cv-quali?ed) class type without a user-provided or deleted default constructor, then the object is zero-initialized and the semantic constraints for default-initialization are checked, and if T has a non-trivial default constructor, the object is default-initialized


        Зачем мне нужно это выяснять в деталях, когда и так ясно, что проще всегда POD-поля ин-плейс инициализировать от греха подальше? Спор с коллегой, который ссылается на эту тонкость стандарта!

        Проверяльщик VS 2017 какие-то случаи ловит, какие-то нет, к сожалению. Надо бы мне на досуге освоить проверяльщик Clang, наверно.


  1. kovserg
    07.08.2017 11:09
    -2

    На древнем C можно под любую кофеварку бинарник собрать, в тоже время современный C++ ограничивает только популярными платформами, C++11 под WinXP уже не работает. Т.к. все используют инструменты которые так или иначе собираются C++ то это очень похоже на целенаправленный механизм выдавливания старых платформ. Например PHP и cygwin уже не пускается под XP, хотя особых оснований для этого нет. Собственно это еще одна из причин по которой на новые версии C++ не привлекательны.


    1. sergey_shambir Автор
      07.08.2017 11:38
      +1

      C++11 под WinXP уже не работает

      Visual Studio 2017 умеет собирать под WinXP при использовании тулсета v141_xp. Она поддерживает почти полностью C++11/C++14 и частично C++17, как в ядре языка, так и в STL.


      Есть нюанс: при объявлении любой статической переменной в теле функции Visual Studio генерирует код, использующий thread local переменные, и на WinXP это может вызывать креш внутри DLL, подключённой к "старому" exe, не имеющему поддержки thread local переменных. Проблема и решение описаны на stackoverflow.


      1. kovserg
        07.08.2017 11:55

        По умолчанию оно генерит бинарники не совмистимые с winxp и даже линковщику нельзя указать версию winxp только через editbin. Поэтому всё что будет собираться без будет без поддержки winxp.
        «Visual Studio 2017 умеет собирать под WinXP при использовании тулсета v141_xp» — его специально сделали таким кривым?
        Ладно microsoft, но кроме visual studio есть другие компиляторы. Так вот они наотрез отказываются под winxp компилить и пускаться. К чему бы это? Такая зависимость сильная зависимость от AVX или все резко осознали что без conditional_variables жини нет?


        1. Steed
          07.08.2017 13:46
          +2

          Просто поддержка старой ОС для разработчиков этих "других компиляторов" в меньшем приоритете, чем другие задачи. Вполне разумно при ограниченных ресурсах.


        1. 4144
          14.08.2017 00:02

          mingw на пример, как собирал бинарники, которые работают на xp, так и собирает.
          работают ли новые версии самого mingw под xp не знаю, т.к. использую кросс компиляцию. Но старые версии mingw работали на xp.


          1. kovserg
            14.08.2017 10:15
            -2

            mingw еще пока работает, но в нём нет статического runtime только с msvcrt, а вот cygwin уже нет.
            Незнаю как вам но я наблюдаю такую картину, чем более современный C++ тем больше код становится запутанней и содержит больше ненужного кода. И портирование обратно на C++98 показывает что новые возможности никак не сокращают объём кода, а наоборот его увеличивают, при этом используются для совершенно не значимых мест.


            1. 4144
              14.08.2017 15:53

              Код становится запутанным из-за таких программистов, а не из-за C++ или стандартов.
              Некоторые просто использую все возможные новые фичи языка, где нужно и где не нужно.


    1. F0iL
      07.08.2017 14:58
      +3

      Это не то чтобы нормально, так вообще и должно быть.
      Некоторые старые API и подходы становятся deprecated, некоторые вызовы наоборот добавляются и начинают повсеместно использоваться (как например в kernel32.dll 'EncodePointer'/'DecodePointer') — зачем по умолчанию ориентироваться на устаревшие вещи вместо более современных аналогов (особенно учитывая что это может ухудшить производительность или безопасность релизной версии)?
      Кому нужно — тот включит таргет вручную. Правильный подход.

      Особенно если учесть, что речь идет про систему 16-летней (!) давности, официальная поддержка которой прекращена много лет назад, и к которой даже не выходят фиксы безопасности (что в наше время уже должно настораживать). Никто же не возмущается, что современными версиями компиляторов VS/GCC/etc. невозможно собирать код под DOS, Win 3.1 или System V Unix — почему здесь ситуация чем-то должна отличаться?


      1. kovserg
        07.08.2017 16:27
        -4

        Какие такие подходы изменились. Под DOS, Win3.1 и unix С/С++98 компилятор работает без проблем например digitalmars. При желании можно запустить в виртуалке или в dosbox-е. Почему C++11 так не может. Что такого он использует при генерации кода что ему мешает работать на старых системах?
        «Речь идет про систему 16-летней» ага через 3года и на windows7 перестанет собираться. Хочется привести ссылку на https://geektimes.ru/post/281470/
        Причем тут система, железо то тот же самое и форматы файлов те же. Почему вдруг появилась привязка к системе? Что такого требует «hello world» что ему мешает пускаться на winxp. Тут только одно объяснение — кому-то это выгодно.
        Более того новые редакции C++ вводят эти самые deprecated и не очевидное поведение очень шустро. То что раньше можно было использовать становится UB. Уже кто-то говорил «В случае UB gcc старается генерировать максимально не корректный код». А чудеса оптимизации, когда бесконечные циклы вдруг возвращают управление.
        Старый код который собирался и работал либо уже не собирается, либо собирается, но не пускается на старых системах, либо работает не корректно и надо отлавливать новые баги.


        1. F0iL
          07.08.2017 17:01
          +3

          > Почему C++11 так не может.
          C++11 может. И C++14 может. И даже свежепринятый C++17 может. От стандарта языка тут практически ничего не зависит (разве что какой-нибудь std::thread под DOS'ом реализовать будет не так-то просто). Не могут конкретные компиляторы, а точнее, как уже выше отметили — могут, но не особо хотят, потому что мало кому надо и стоит потратить время и силы на более полезные вещи.

          > Что такого требует «hello world» что ему мешает пускаться на winxp.
          Я уже выше привел пример с EncodePinter/DecodePointer. Бинарники, в которых есть его вызов, не запустятся на SP2. Хотите запускаться на WinXP — соберитесь с более старыми версиями стандартных библиотек, все будет работать. Ради нескольких процентов аудитории (которых легко вернуть просто выбрав нужный таргет при компиляции) терять в производительности или безопасности для всех остальных пользователей — как-то глупо.

          > Более того новые редакции C++ вводят эти самые deprecated и не очевидное поведение очень шустро
          О причинах подобного обычно очень подробно пишут в proposal'ах к стандарту и документации. Все имеет обоснование.


          1. kovserg
            07.08.2017 17:20
            -1

            «Ради нескольких процентов аудитории» — C++ стала попсой и расчитанной на 98% аудитории?
            «терять в производительности или безопасности для всех остальных пользователей — как-то глупо» и в каком месте выросла безопасность и увеличилась производительность?


            1. F0iL
              07.08.2017 17:43
              +1

              > «Ради нескольких процентов аудитории» — C++ стала попсой и расчитанной на 98% аудитории?
              замените слово «аудитории» на «платформ/операционных систем/сред исполнения», надеюсь, смысл фразы станет понятнее.

              > в каком месте выросла безопасность и увеличилась производительность?
              выше был конкретный пример, читайте комментарии внимательнее.


              1. kovserg
                07.08.2017 18:30
                -1

                > замените слово «аудитории» на «платформ/операционных систем/сред исполнения», надеюсь, смысл фразы станет понятнее.
                Да. То есть среда для ширпотреба, но для этого есть другие языки. более «безопасные» и «управляемые»

                > читайте комментарии внимательнее
                Внимательно прочитал EncodePinter/DecodePointer у которых жутко сложная реализация примерно такого вида ptr^=CONST очень сильно увеличивают безопасность и производительность и требуют «Minimum supported client: WindowsXP SP2»


                1. DistortNeo
                  07.08.2017 19:17
                  +1

                  1. kovserg
                    07.08.2017 22:52
                    -1

                    И что это делает абсолютно не возможным запуск компилятора? Какое отношение это имеет к компилятору? Его задача строить AST деревия и из них собирать бинарники. А тут целеноправлено линкеру запрещено указывать SYSTEM и SUBSYSTEM ниже Vist-ы. Каким боком тут новые функции user32? Неужели компилятор использовал именно эти методы которые удалили? Или не может работать без свеже добавленных? Накой хрен спрашивается в винде директория WinSxS?


                    1. Kobalt_x
                      08.08.2017 22:02

                      1)WinSxS не для системных библиотек. А для библиотек стороннего По(в том числе и от МS)
                      2)В части stl работа с потоками/локалями может быть завязка на функции из нового winsdk, поэтому там скорее всего тоже будет #ifdef на версию кодогенератора.


                      3)Кодогенерация для этих подсистем происходит по разному(см EncodePointer да там xor, но xor с кукой из PEB которой тупо нет в not 5.x) Поэтому и запрещают старый subsystem. Если же так нужно используйте тулчейн с суффиксом _xp в котором компоновщик(который может полагаться на новые фичи pe loader а из новых версий windows) и кодогенератор будут работать по-старому


                      1. kovserg
                        09.08.2017 02:10
                        -1

                        Видимо я плохо пишу по русски. Дело не в кодогенерации и не в EncodePointer, а в целенаправленном выпиливании поддержки определённых платформ. Это динамические библиотеки что мешает при отсутствии функции не использовать её или использовать аналог. И потом зачем EncodePointer если есть EncodeRemotePointer сразу привязываемся к Win10.

                        Что печально именно такое поведение и будет в дальнейшем. Я не могу понять почему это всех устраивает. Вам ограничивают свободу действий и все хлопают в ладошки и просят добавки. При этом так ведут себя все не только m$ но и goolge не отстаёт.

                        Компилятор не должен быть завязан на платформу, на неё должны быть завязаны библиотеки, которые можно спокойно менять. А тут именно компилятор гвоздями прибивают. Именно C++ который участвует в сборке практически всё. И это остальное автоматом работает только на новой платформе. Любые попытки портировать обратно начинают требовать всё возрастающих затрат затрат. И логично что этим никто-заниматься не будет.

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

                        Если собирать проект в VS2005 то результат пускается как на win10 так и на winnt4, если еще и манифест добавить то даже будет красиво.

                        Но всем подавай новые свистелки, в результате так тихо и незаметно всех ведут к светлому будущему WinX. В итоге новые версии каких-нибудь php, perl, python или putty будет пускаться только в WinX.


                        1. Kobalt_x
                          09.08.2017 06:53

                          Ну вы же не жалуетесь что msvc aka cl.exe не может юзать другой stdlib кроме msvcrt? Не возмущается почему icc на sparc не работает и powerpc. Чем меньше платформ у компилятора с тем лучше он оптимизирует для них код. И если для поддержки старых платформ нужно тащить старые костыли, то уж лучше выделить эти костыли в отдельную версию кодогенератора, стоит сделала ms. С ним у вас как раз будут все фичи новых стандартов на winxp. Да Такова разработчик компилятора завязался на свою стандартную библиотеку. MinGw может линковаться статически libstdc++ и libgcc (-static-libstdc++ -static-libgcc).Вместо msvcrt можно использовать newlib, если что. P.S. EncodeRemotePointer нужен только для rpc


                          1. kovserg
                            09.08.2017 09:32
                            -2

                            Просто не используя новые фичи C++ всё по прежнему работает на всех виндах. А есть новые, то только win7+ и в последствии только winx+. Стоят новые фичи этого? Если что-то общего пользования делать, то С++ как кандидат отпадает и остаётся только C но более консервативен. Например lua написана на C и его без проблем можно собрать под что угодно, хоть под дос и оно будет работать на каком-нибудь ICPDAS-е.


                    1. Whuthering
                      09.08.2017 16:55

                      Вы издеваетесь, или серьёзно не понимаете? Причём здесь user32, есть и более низкоуровневые API (типа упомянутой выше ntoskrnl32), и у компилятор их использует при генерации кода, либо же они задействованы в определённых методах стандартной библиотеки. Они не критически необходимы, но с ними очевидно лучше — не зря же, в конце концов, их придумали и используют.
                      То, что какой-то софт работает после простой смены версии subsystem — никакой не заговор, а простое везение, и никто не гарантирует, что все не перестанет работать если разработчик задействует ещё какой-нибудь другой метод из стандартной библиотеки. Да, можно собираться по умолчанию в режиме "для самой древности", забив на все улучшения, можно задействовать костыли в виде прослоек совместимости или разводить зоопарк библиотек разных версий в системе — но зачем? Ради крохотной горстки пользователей морально устаревших систкм, для которых при крайней необходимости можно сделать рабочий билд просто за одну минуту поменяв одну настройку компилятора? Честное слово, дико подобное читать в нашем время на техническом ресурсе, да и вообще такой "вернем все в зад!" подход самым прямым образом ведёт к появлению нестабильного и тормозного ПО.


                      1. kovserg
                        09.08.2017 17:42
                        -1

                        Да издеваюсь. Все критически необходимые функции никто не менял. Подход называется не «вернём всё в зад», а «не стоит сжигать мосты».


    1. ZaMaZaN4iK
      07.08.2017 15:44

      Вы утверждаете, что новый С++ не подходит для множества платформ? Посмотрите, под что умеет компилировать GCC, и почитайте кучу историй о том, как применяют С++ на МК. Всё там нормально с этим.


      1. lorc
        07.08.2017 16:20

        Мы применяли какое-то время. Включение исключений увеличивало код на 20%. Стандартную библиотеку использовать тоже было практически невозможно, потому что она раздувала код очень сильно. И жрала RAM, которой было не очень то и много.

        Поэтому, исключения мы всё-таки оставили, но STL не использовали, реализовывали нужные примитивы вручную.

        Что бы вы понимали, мы в 512кб ROM и 128кб RAM упихали RTOS, сетевой стек, графическую подсистему, файловую систему, криптографию, поддержку Unicode, драйвера для всякой периферии и собственно бизнес-логику.

        На самом деле у нас только бизнес-логика была написана на С++, что бы упростить жизнь прикладным программистам. Если бы мы всё писали на С++, то оно просто не поместилось бы в МК,


        1. F0iL
          07.08.2017 17:08

          То что исключения увеличивают размер кода, в принципе, ожидаемо, учитывая что практически везде сейчас используются zero-cost exceptions — в извесном споре оптимизаций «производительность/размер кода» этот подход ставит на первое. Раньше gcc можно было принудительно заставить использовать вариант setjmp/longjmp обработчика исключений (меньше размер кода, но появляются накладные расходы на саму обработку исключений), сейчас не знаю.


      1. kovserg
        07.08.2017 17:10

        Вот тут путаница C++ уже давно не C++ а C++11, C++14, C++17, C++20…
        Под линуксом такого безобразия нет как под виндой и там gcc работает и кроскомпилирует. А в винде нет.


  1. RomanArzumanyan
    07.08.2017 11:40
    +2

    const auto book = BookStats{
        .title = u8"Незнайка на Луне",
        .authors = { u8"Николай Носов" },
        .tags = { u8"детская", u8"фантастика" },
        .publishingYear = 1965,
        .pageCount = 576
    };
    


    Наконец, вернули то, что в Си было давным-давно!


    1. kovserg
      07.08.2017 12:12
      +3

      Теперь можно объявлять вот такие фунцкии:

      struct Add {
          double x=0, y=0;
          operator double () { return x+y; }
      };
      
      double z=Add{ .y=20, .x=10 };
      


      1. bfDeveloper
        07.08.2017 12:26

        Отредактировано
        Вероятно нужны ещё скобочки, чтобы вызвать функцию на созданном объекте:

        double z=Add{ .y=20, .x=10 }();
        

        Перепутал оператор double с оператором (). Вот такое приведение кажется опасным. Ещё никогда операторы неявного приведения до добра не доводили.
        Но выглядит очень круто, никогда не смотрел на именованные аргументы с этой стороны.


        1. kovserg
          07.08.2017 18:42

          Было бы круто если бы так работало

          struct Fn {
              int x; enum { B0=1, B1=2, B2=4, B3=8 };
              operator int () { return x; }
          };
          
          int z=Fn{ .x=B2|B3 }
          


          1. ExModE
            09.08.2017 16:55

            Таким образом разве что.

            namespace bit_placeholders {
              enum { B0=1, B1=2, B2=4, B3=8 };
            }
            
            struct Fn {
              int x;
              constexpr operator int() { return x; }
            };
            
            int main() {
              using namespace bit_placeholders;
            
              constexpr int z=Fn{ .x=B2|B3 };
            
              static_assert(z == 12, "");
            }
            


    1. lorc
      07.08.2017 14:57

      Так не вернули же. Только собираются в С++20.


  1. kovserg
    07.08.2017 12:30

    не скобки не нужны. http://ideone.com/R4Djyv



  1. Steed
    07.08.2017 13:55

    Спасибо за последний пример с std::variant. А то я все думал, как в С++ сделать красивые algebraic data types вроде Haskell'овского Maybe (Nothing | Just a).


    1. GraD_Kh
      08.08.2017 10:51
      +1

      Разве это не std::optional. Так же стоит указать, что optional, variant, any и string_view(string_ref) давно есть в boost, так что можно не ждать 17-го стандарта, а уже использовать.


      1. Steed
        08.08.2017 11:14

        Сам Maybe — это optional, конечно. Но есть иногда желание возвращать что-то более хитрое, самый банальный пример (из статьи) — результат | код ошибки с сообщением. Либо хранить состояние вместе с его параметрами, но не делая под это полноценный класс.


  1. 0xd34df00d
    09.08.2017 01:28

    // C++11, C++14: будет ошибка компиляции из-за инициализаторов pageCount и publishingYear

    Вы уверены? В C++14 ЕМНИП это поправили, и ошибкой компиляции это не будет уже там. Проверка с clang 4 и -std=c++11 против c++14 это подтверждает.