Апрель 2016

Том 31 номер 4

Visual C++ - Microsoft двигает C++ в будущее

Кенни Керр | Апрель 2016

Продукты и технологии:

Visual C++ Update 2

В статье рассматриваются:

  • модульная система для C++;
  • внутри сопрограмм (coroutines);
  • вклад Microsoft в развитие C++.

Visual C++ имеет репутацию отстающего. Если вам нужны самые новые и потрясающие языковые средства C++, вы должны просто использовать Clang или GCC, или нечто в этом роде, так что часто повторяющая история продолжается. Мне бы хотелось предположить, что вскоре произойдет какая-то перемена в существующем положении дел, сбой в матрице, воздействие внешних факторов, если хотите. Это верно, что у компилятора Visual C++ невероятно старая кодовая база, которая затрудняет группе C++ в Microsoft быстрое добавление новых языковых средств (goo.gl/PjSC7v). Однако эта ситуация начинает меняться, причем Visual C++ становится «нулевой отметкой» для многих новых предложений в языке C++ и Standard Library. Я собираюсь выделить несколько новых или улучшенных средств в выпуске Visual C++ Update 2, которые я нашел особенно интересными и которые иллюстрируют, что жизнь еще теплится в этом видавшем виды компиляторе.

Модули

Несколько разработчиков в Microsoft, в частности Гэбриель Дос Рейс (Gabriel Dos Reis) и Джонатан Кавиш (Jonathan Caves), уже работают над архитектурой, которая позволяет напрямую поддерживать компонентное представление из языка C++. Дополнительная цель — улучшить пропускную способность компиляции по аналогии с применением заранее скомпилированного заголовочного файла (precompiled header). Эта архитектура, названная модульной системой для C++, была предложена для стандарта C++17, и новый компилятор Visual C++ предоставляет прототип этой концепции и рабочую реализацию для модулей в C++. Модули проектируются так, чтобы любому разработчику, применяющему стандартный C++, было очень просто и естественно создавать и использовать их. Убедитесь, что вы установили Visual C++ Update 2, откройте командную строку разработчика и следуйте за мной. Так как это средство все еще находится на экспериментальной стадии, оно не поддерживается IDE, и самый лучший способ приступить к работе с ним — задействовать компилятор прямо из командной строки.

Давайте представим, что у меня есть существующая C++-библиотека, которую я хотел бы распространять как модуль примерно так:

C:\modules> type animals.h
#pragma once
#include <stdio.h>

inline void dog()
{
  printf("woof\n");
}

inline void cat()
{
  printf("meow\n");
}

Возможно, у меня есть и превосходное приложение-пример, сопутствующее моей доморощенной библиотеке:

C:\modules> type app.cpp
#include "animals.h"

int main()
{
  dog();
  cat();
}

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

C:\modules> type animals.ixx
module animals;
#include "animals.h"

Конечно, я мог бы просто определить функции cat и dog прямо в файле интерфейса модуля, но их включение работает не менее хорошо. Объявление модуля сообщает компилятору: далее следует то, что является частью модуля, но это не означает, что все последующие объявления экспортируются как часть интерфейса модуля. На данный момент этот модуль ничего не экспортирует, если только заголовочный файл stdio.h, включенный в заголовочный файл animals.h, сам что-то не экспортирует. Я даже могу предотвратить это, включив stdio.h до объявления модуля. Итак, если этот интерфейс модуля на самом деле не объявляется никаких открытых имен, как же экспортировать что-то для использования другими? Мне нужно задействовать ключевое слово export; это и еще два ключевых слова — module и import — вот и все, что добавлено в язык C++. Это говорит об изящной простоте нового языкового средства.

Объявление модуля сообщает компилятору: далее следует то, что является частью модуля, но это не означает, что все последующие объявления экспортируются как часть интерфейса модуля.

Для начала можно экспортировать функции cat и dog. Это включает обновление заголовочного файла animals.h, а начинать оба объявление нужно со спецификатора export:

C:\modules> type animals.h
#pragma once
#include <stdio.h>

export inline void dog()
{
  printf("woof\n");
}

export inline void cat()
{
  printf("meow\n");
}

Теперь я могу скомпилировать файл интерфейса модуля, используя ключ компилятора для модулей:

C:\modules> cl /c /experimental:module animals.ixx

Заметьте, что я также указал ключ /c, чтобы проинструктировать компилятор просто скомпилировать код, но не компоновать его. На этой стадии нет смысла, чтобы компоновщик (linker) пытался создать исполняемый файл. Ключ module сообщает компилятору выдать файл, содержащий метаданные, которые описывают интерфейс и реализацию модуля в двоичном формате. Эти метаданные являются не машинным кодом, а двоичным представлением языковых конструкций C++. Однако это и не исходный код, что и хорошо, и плохо — в зависимости от того, как вы смотрите на это. Хорошо в том плане, что эти метаданные должны увеличить пропускную способность процесса сборки, поскольку приложения, которые могут импортировать этот модуль, не потребуют заново разбирать этот код. С другой стороны, эта также означает, что возможное отсутствие какого-либо исходного кода для традиционных инструментов вроде Visual Studio и ее механизма IntelliSense с целью его визуализации и синтаксического разбора. То есть Visual Studio и другие инструменты нужно научить тому, как анализировать и визуализировать код внутри модуля. Хорошая новость в том, что код или метаданные внутри модуля хранятся в открытом формате и инструментарий можно соответственно обновить.

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

C:\modules> type app.cpp
import animals;

int main()
{
  dog();
  cat();
}

Объявление import указывает компилятору искать соответствующий файл интерфейса модуля. Затем он может использовать его наряду с любыми другими включаемыми файлами, которые могут присутствовать в приложении, чтобы разрешить функции dog и cat. К счастью, модуль animals экспортирует пару «пушистых» функций, и приложение можно заново скомпилировать, используя тот же ключ module в командной строке:

C:\modules> cl /experimental:module app.cpp animals.obj

Заметьте, что на этот раз я разрешил компилятору вызвать компоновщик, поскольку теперь я хочу получить исполняемый файл. Ключ experimental:module по-прежнему необходим, поскольку ключевое слово import пока не является официальным. Более того, компоновщику также требуется создание объектного файла при компиляции модуля. Это вновь намекает на тот факт, что новый двоичный формат, в котором содержатся метаданные модуля, на самом деле является не кодом, а описанием экспортируемых объявлений, функций, классов, шаблонов и т. д. К моменту, когда вы захотите собрать приложение, использующее этот модуль, вам все равно понадобится объектный файл, чтобы компоновщик мог выполнить свою работу по сборке кода в исполняемый файл. Если все прошло успешно, я получаю исполняемый файл, который можно запускать, как любой другой исполняемый файл, — конечный результат ничем не отличается от исходного приложения, использовавшего библиотеку только как заголовочный файл. Иначе говоря, модуль не является DLL.

Сейчас я работаю над довольно большой библиотекой, и мысль насчет добавления export в каждое объявление не кажется сколько-нибудь привлекательной. К счастью, объявление export позволяет экспортировать нечто большее, чем просто функции. Один из вариантов — экспорт группы объявлений с помощью фигурных скобок:

C:\modules> type animals.h
#pragma once
#include <stdio.h>

export
{
  inline void dog()
  {
    printf("woof\n");
  }

  inline void cat()
  {
    printf("meow\n");
  }
}

Такой вариант не вводит новую область видимости, а просто используется для группирования любых заключенных в скобки объявлений для экспорта. Конечно, ни один уважающий себя программист на C++ не стал бы писать библиотеку с пачкой объявлений на глобальном уровне. Гораздо вероятнее, что мой заголовочный файл animals.h объявлял бы функции dog и cat внутри какого-то пространства имен и это пространство имен в целом можно было бы весьма легко экспортировать:

C:\modules> type animals.h
#pragma once
#include <stdio.h>

export namespace animals
{
  inline void dog()
  {
    printf("woof\n");
  }

  inline void cat()
  {
    printf("meow\n");
  }
}

Другое неявное преимущество в переходе с библиотеки, содержащей только заголовочный файл (header-only library), на модуль заключается в том, что приложение больше не сможет случайно получить зависимость от stdio.h, поскольку тот не является частью интерфейса модуля. Как быть, если моя библиотека, содержащая только заголовочный файл, включает некое вложенное пространство имен, где находятся детали реализации, не предназначенные для прямого использования приложениями? На рис. 1 показан типичный пример такой библиотеки.

Рис. 1. Библиотека, содержащая только заголовочный файл, с пространством имен реализации

C:\modules> type animals.h
#pragma once
#include <stdio.h>

namespace animals
{
  namespace impl
  {
    inline void print(char const * const message)
    {
      printf("%s\n", message);
    }
  }

  inline void dog()
  {
    impl::print("woof");
  }

  inline void cat()
  {
    impl::print("meow");
  }
}

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

C:\modules> type app.cpp
#include "animals.h"
using namespace animals;
int main()
{
  dog();
  cat();
  impl::print("rats");
}

Могут ли здесь помочь модули? Безусловно, но учтите, что принцип модулей основан на удержании некоей функциональности максимально малой или простой. Поэтому после экспорта объявления остальное тоже экспортируется без вариантов:

C:\modules> type animals.h
#pragma once

#include <stdio.h>

export namespace animals
{
  namespace impl
  {
    // Жаль, но это тоже экспортируется
  }

  // Это экспортируется
}

К счастью, как показано на рис. 2, вы можете переупорядочить код так, чтобы пространство имен animals::impl объявлялось отдельно, в то же время сохранив структуру пространств имен библиотеки.

Рис. 2. Сохранение структуры пространств имен библиотеки

C:\modules> type animals.h
#pragma once
#include <stdio.h>

namespace animals
{
  namespace impl
  {
    // Это больше не экспортируется - ура!
  }
}

export namespace animals
{
  // Это экспортируется
}

Теперь нам нужно, только чтобы Visual C++ реализовал определения вложенных пространств имен, и тогда становится гораздо приятнее смотреть на код и гораздо легче управлять библиотеками со множеством вложенных пространств имен:

C:\modules> type animals.h
#pragma once
#include <stdio.h>

namespace animals::impl
{
  // Это не экспортируется
}

export namespace animals
{
  // Это экспортируется
}

Хотелось бы надеяться, что эта возможность появится в Visual C++ Update 3. Держите пальцы скрещенными! На данном этапе заголовочный файл animals.h разрушит существующие приложения, которые просто включают заголовочный файл и, возможно, компилируются с помощью компилятора, пока не поддерживающего модули. Если вам нужна поддержка существующих пользователей библиотеки и в то же время вы хотите постепенно переводить их на модули, то можно задействовать «страшный» препроцессор, чтобы сгладить шероховатости на время перехода. Это не идеальное решение. Дизайн многих более новых языковых средств C++, включая модули, подразумевает программирование на C++ без макросов. Тем не менее, пока модули действительно не попадут в стандарт C++17 и пока разработчикам не станут доступны коммерческие реализации, можно использовать немного трюкачества препроцессора, чтобы заставить компилировать библиотеку animals и как только заголовочный файл, и как модуль. Внутри своего заголовочного файла animals.h я могу определять по условию макрос ANIMALS_EXPORT как nothing и использовать его так, чтобы он предшествовал любым пространствам имен, которые я хотел бы экспортировать так, будто это модуль (рис. 3).

Рис. 3. Компиляция библиотеки и как только заголовочного файла, и как модуля

C:\modules> type animals.h
#pragma once
#include <stdio.h>

#ifndef ANIMALS_EXPORT
#define ANIMALS_EXPORT
#endif

namespace animals { namespace impl {

// Пожалуйста, сюда не заглядывайте

}}

ANIMALS_EXPORT namespace animals {

// А это все ваше

}

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

C:\modules> type animals.ixx
module animals;

#define ANIMALS_EXPORT export

#include "animals.h"

Подобно многим современным разработчикам на C++ я не люблю макросы и предпочел бы жить в мире, где их нет. Тем не менее, это полезный метод, когда вы переводите библиотеку в модули. Самое лучшее в том, что, хотя приложение, которое включает заголовочный файл animals.h, будет видеть благодатный макрос, последний будет вообще невидим тем, кто просто импортирует модуль. Макрос выбрасывается до создания метаданных модуля и в итоге никогда не попадет в приложение или в любые другие библиотеки и модули, которые могут его использовать.

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

Модули являются желанным дополнением в C++, и я с нетерпением жду будущего обновления компилятора с полной коммерческой поддержкой модулей. А пока вы можете экспериментировать вместе с нами, пока мы проталкиваем систему модулей в стандарт C++. Узнать больше о модулях можно из технической спецификации (goo.gl/Eyp6EB) или посмотрев выступление Гэбриеля Дос Рейса на CppCon в прошлом году (youtu.be/RwdQA0pGWa4).

Сопрограммы

Хотя сопрограммы (coroutines), ранее называвшиеся возобновляемыми функциями (resumable functions), уже какое-то время присутствуют в Visual C++, я по-прежнему с нетерпением ожидаю появления языковой поддержки настоящих сопрограмм, которые своими корнями уходят глубоко в проектное решение на основе стеков для языка C. Когда я обдумывал, что написать, меня осенило, что на эту тему я написал для журнала «MSDN Magazine» минимум четыре статьи. Предлагаю вам начать с самой недавней статьи в номере за октябрь 2015 года (goo.gl/zpP0AO), где вы найдете введение в поддержку сопрограмм, предоставляемую Visual C++ 2015. Вместо того чтобы переливать из пустого в порожнее, снова описывая преимущества сопрограмм, давайте лучше копнем их немного поглубже. Одна из проблем с принятием сопрограмм в C++17 — комитету по стандартизации не понравилась идея того, что эти сопрограммы могли бы автоматически обеспечивать логическое распознавание типа. Тип сопрограммы может быть логически распознан компилятором, благодаря чему разработчику не придется раздумывать над тем, какой у нее тип:

auto get_number()
{
  await better_days {};
  return 123;
}

Компилятор вполне способен создать подходящий тип сопрограммы и бесспорно, что это было навеяно стандартом C++14, в котором заявлялось, что возвращаемый тип функций можно распознать логически:

auto get_number()
{
  return 123;
}

Тем не менее, комитет по стандартизации пока не уверен, что эту идею стоит распространять на сопрограммы. Проблема в том, что C++ Standard Library не предлагает подходящих кандидатов. Самое близкое приближение — громоздкая конструкция std::future с ее зачастую тяжелой реализацией и очень непрактичным проектным решением. Кроме того, она не особо поможет в случае асинхронных потоков данных, создаваемых сопрограммами, которые асинхронно возвращают не просто одно значение, а наборы значений. Поэтому, если компилятор не может предоставить тип, равно как и C++ Standard Library, мне нужно несколько внимательнее посмотреть, как это работает на самом деле, если я намерен добиться какого-то прогресса с сопрограммами. Вообразите, что у меня есть следующий ожидаемый тип (awaitable type):

struct await_nothing
{
  bool await_ready() noexcept
  {
    return true;
  }

  void await_suspend(std::experimental::
    coroutine_handle<>) noexcept
  {}

  void await_resume() noexcept
  {}
};

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

coroutine<void> hello_world()
{
  await await_nothing{};
  printf("hello world\n");
}

И вновь, если я не могу положиться на то, что компилятор автоматически распознает тип возврата сопрограммы, и если я предпочитаю не использовать std::future, как же мне определить следующий шалон класса сопрограммы?

template <typename T>
struct coroutine;

Поскольку место в статье ограничено, давайте рассмотрим пример с сопрограммой, которая ничего не возвращает, т. е. является void. Вот специализация:

template <>
struct coroutine<void>
{
};

Первое, что делает компилятор, — ищет promise_type в типе возврата сопрограммы. Есть и другие способы связывания этого, особенно если вам надо включить поддержку сопрограмм в существующую библиотеку, но, поскольку я пишу шаблон класса coroutine, я могу просто объявить его прямо здесь:

template <>
struct coroutine<void>
{
  struct promise_type
  {
  };
};

Далее компилятор ищет функцию return_void в обещании (promise) сопрограммы, по крайней мере для тех сопрограмм, которые не возвращают никакого значения:

struct promise_type
{
  void return_void()
  {}
};

Хотя return_void не требуется что-либо делать, она может быть задействована в других реализациях как сигнал об изменении состояния, сообщающий, что логический результат сопрограммы готов и что его можно проанализировать. Компилятор также ищет пару функций initial_suspend и final_suspend:

struct promise_type
{
  void return_void()
  {}

  bool initial_suspend()
  {
    return false;
  }

  bool final_suspend()
  {
    return true;
  }
};

Компилятор использует их для встраивания начального и заключительного кода в сопрограмму, что сообщает планировщику, следует ли запускать сопрограмму в приостановленном состоянии и надо ли приостанавливать ее перед завершением. Эта пара функций на самом деле может возвращать ожидаемые типы (awaitable types), чтобы в итоге компилятор мог ожидать на них обеих следующим образом:

coroutine<void> hello_world()
{
  coroutine<void>::promise_type & promise = ...;
  await promise.initial_suspend();

  await await_nothing{};
  printf("hello world\n");

  await promise.final_suspend();
}

Требуется ли ожидание и соответственно встраивание точки приостановки (suspension point), зависит от того, чего вы пытаетесь добиться. В частности, если вам нужно запрашивать сопрограмму по ее завершении, вы предпочтете наличие заключительной приостановки; в ином случае сопрограмма будет уничтожена до того, как вы получите шанс запросить любое значение, захваченное обещанием.

Затем компилятор анализирует, как получить объект coroutine от обещания:

struct promise_type
{
  // ...

  coroutine<void> get_return_object()
  {
    return ...
  }
};

Компилятор удостоверяется, что promise_type создан как часть фрейма coroutine. Потом ему понадобится способ создать тип возврата сопрограммы из этого обещания. После чего он возвращается вызвавшему коду. Здесь я должен опираться на очень низкоуровневый вспомогательный класс, предоставляемый компилятором и называемый coroutine_handle; в настоящее время он содержится в пространстве имен std::experimental. Класс coroutine_handle представляет один вызов сопрограммы; таким образом, я могу сохранить этот описатель как член моего шаблона класса coroutine:

template <>
struct coroutine<void>
{
  // ...

  coroutine_handle<promise_type> handle { nullptr };
};

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

Я инициализирую описатель nullptr, чтобы указать, что сопрограмма в настоящее время не выполняется, но могу и добавить конструктор для явного связывания описателя с только что сконструированной сопрограммой:

explicit coroutine(coroutine_handle<promise_type> coroutine) :
  handle(coroutine)
{}

Фрейм сопрограммы — это нечто вроде фрейма стека, но является динамически выделяемым ресурсом, и его нужно уничтожать, поэтому, естественно, я пишу деструктор для этой цели:

~coroutine()
{
  if (handle)
  {
    handle.destroy();
  }
}

Мне также нужно удалить операции копирования и разрешить семантику перемещения — вы, очевидно, поняли идею. Теперь я могу реализовать функцию get_return_object класса promise_type, которая будет выступать в роли фабрики объектов coroutine:

struct promise_type
{
  // ...

  coroutine<void> get_return_object()
  {
    return coroutine<void>(
      coroutine_handle<promise_type>::from_promise(this));
  }
};

Теперь компилятору должно быть достаточно, чтобы создать сопрограмму и вызвать ее к жизни. Здесь за coroutine снова следует простая функция main:

coroutine<void> hello_world()
{
  await await_nothing{};
  printf("hello world\n");
}

int main()
{
  hello_world();
}

Я пока ничего не сделал с результатом hello_world, но выполнение этой программы приведет к вызову printf, и в консоли появится знакомое сообщение. Означает ли это, что сопрограмма действительно завершена? Что ж, об этом я могу спросить у самой сопрограммы:

int main()
{
  coroutine<void> routine = hello_world();
  printf("done: %s\n", routine.handle.done() ? "yes" : "no");
}

На этот раз я ничего не делаю с сопрограммой, но спрашиваю, завершена ли она, и, конечно же, она завершена:

hello world
done: yes

Вспомните, что функция initial_suspend класса promise_type возвращает false, поэтому сопрограмма сама по себе не начинает свою жизнь в приостановленном состоянии. Также вспомните, что функция await_ready класса await_nothing возвращает true, чтобы не вводить точку приостановки. Конечный результат — сопрограмма, которая выполняется синхронно, поскольку я не дал ей указания действовать иначе. Изящество всего этого в том, что компилятор способен оптимизировать сопрограммы с синхронным поведением и применять к ним все те же оптимизации, которые делают столь быстрыми линейные участки кода (straight-line code). Тем не менее, это не очень интересно, поэтому давайте добавим приостановку или, по крайней мере, какие-то точки приостановки. Для этого достаточно изменить тип await_nothing так, чтобы он всегда вызывал приостановку, хотя делать ему совершенно нечего:

struct await_nothing
{
  bool await_ready() noexcept
  {
    return false;
  }

  // ...
};

В этом случае компилятор увидит, что этот ожидаемый объект не готов, и вернет управление вызвавшему коду до возобновления. Теперь, если вернуться к моему простому приложению «hello world»:

int main()
{
  hello_world();
}

Вы увидите, что эта программа ничего не выводит. Причина должна быть очевидна: сопрограмма приостановлена до вызова printf, а вызвавший код, которому принадлежит объект coroutine, не дал ей никакого шанса на возобновление. Естественно, возобновить сопрограмму так же просто, как вызвать функцию resume, предоставляемую handle:

int main()
{
  coroutine<void> routine = hello_world();
  routine.handle.resume();
}

Теперь функция hello_world снова возвращает управление без вызова printf, но функция resume заставит сопрограмму выполняться. Для более наглядной иллюстрации можно использовать метод done из handle до и после возобновления:

int main()
{
  coroutine<void> routine = hello_world();
  printf("done: %s\n", routine.handle.done() ? "yes" : "no");
  routine.handle.resume();
  printf("done: %s\n", routine.handle.done() ? "yes" : "no");
}

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

done: no
hello world
done: yes

Это может быть очень удобно, особенно во встраиваемых системах, в которых отсутствуют изощренные планировщики и потоки ОС, поскольку можно довольно легко написать облегченную систему с кооперативной многозадачностью (cooperative multitasking):

while (!routine.handle.done())
{
  routine.handle.resume();
  // Выполняем другую интересную работу...
}

В сопрограммах нет ничего волшебного, и для работы они не требуют сложной логики планирования или синхронизации. Поддержка сопрограмм с типами возврата включает замену функции return_void класса promise_type функцией return_value, которая принимает значение и сохраняет его в обещании. Затем вызвавший код может получить это значение по завершении сопрограммы. Сопрограммы, которые выдают поток значений, требуют похожей функции yield_value в promise_type, но в остальном все, по сути, одинаково. Точки подключения, предоставляемые компилятором для сопрограмм, довольно просты и в то же время удивительно гибки. Я лишь поверхностно затронул эту тематику в своем кратком обзоре, но надеюсь, что вы получили представление об этом потрясающем новом языковом средстве.

Гор Нишанов (Gor Nishanov), другой разработчик из группы C++ в Microsoft, продолжает продвигать сопрограммы к окончательной стандартизации. Он работает даже над добавлением поддержки сопрограмм в компилятор Clang! Чтобы узнать больше о сопрограммах, прочитайте техническую спецификацию (goo.gl/9UDeoa) или посмотрите выступление Нишанова на конференции CppCon в прошлом году (youtu.be/_fu0gx-xseY). Джеймс Макнеллис (James McNellis) также рассказывал о сопрограммах на конференции Meeting C++ (youtu.be/YYtzQ355_Co).

В Microsoft сейчас идет куда более масштабная работа над C++. Мы добавляем новые языковые средства C++, в том числе шаблоны переменных (variable templates) из C++14, позволяющие вам определять семейство переменных (goo.gl/1LbDJ2). Нил Макинтош (Neil MacIntosh) работает над новыми предложениями в C++ Standard Library для представлений строк и последовательностей с безопасными границами (bounds-safe). Вы можете почитать о span<> и string_span на goo.gl/zS2Kau и goo.gl/4w6ayn и даже увидеть их реализацию в GitHub.com/Microsoft/GSL.

В итоге я недавно обнаружил, что оптимизатор C++ гораздо интеллектуальнее, чем я думал, когда дело доходит до оптимизации вызовов strlen и wcslen со строковыми литералами. Это не особенно новые оптимизации, хоть и является хорошо защищаемым секретом. Новое в том, что Visual C++ наконец реализует полную оптимизацию пустых базовых классов (complete empty base optimization), которой не хватало уже больше десятилетия. Применение __declspec(empty_bases) к классу приводит к тому, что все пустые базовые классы размещается с нулевым смещением. Это пока не делается по умолчанию, поскольку потребовало бы большого обновления компилятора для введения такого разрушающего изменения, и, кроме того, некоторые типы в C++ Standard Library по-прежнему полагаются на старое размещение. Тем не менее, разработчики библиотек наконец-то смогут задействовать преимущества этой оптимизации. Современный C++ для Windows Runtime (moderncpp.com) особенно выигрывает от этой оптимизации и является реальной причиной того, почему эта функциональность была добавлена в компилятор. Как я упоминал в своей статье за декабрь 2015 года, я недавно присоединился к группе Windows в Microsoft для создания новой языковой проекции для Windows Runtime, основанной на moderncpp.com, и это тоже помогает продвигать C++ в Microsoft. Будьте уверены, что Microsoft очень серьезно настроена по отношению к C++.


Кенни Керр (Kenny Kerr) — инженер программного обеспечения в группе Windows в компании Microsoft. Ведет блог kennykerr.ca. Кроме того, читайте его заметки в Twitter (@kennykerr).

Выражаю благодарность за рецензирование статьи эксперту Microsoft Эндрю Парду (Andrew Pardoe).


Discuss this article in the MSDN Magazine forum