2016 年 4 月

第 31 卷,第 4 期

本文章是由機器翻譯。

Visual C++ - Microsoft 將 C++ 推向未來

Kenny Kerr | 2016 年 4 月

Visual c + + 有落後曲線的評價。如果您需要的最新和最大 c + + 功能,您應該只使用 Clang 或 GCC,或讓狂重複故事起源。我想如果您將會受到現狀、 在矩陣碰上騷動強制變更的建議。Visual c + + 編譯器有非常舊的程式碼基底已經使 c + + 小組來快速新增功能的 microsoft 的確 (goo.gl/PjSC7v)。這開始變更,不過,Visual c + + 所提議的 c + + 語言和標準程式庫的許多新的地面零。我要反白顯示一些新或 Visual c + + Update 2 中的增強的功能會釋放,我發現特別吸引人且說明生命尚未中沒有這個正式的編譯器。

模組

少數的開發人員在 Microsoft,尤其是 Gabriel Dos Reis 和 Jonathan Caves 一直使用以元件化支援直接新增至 c + + 語言的設計。次要的目標是要提高建置輸送量,類似於先行編譯標頭。這項設計,呼叫 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");
}

我也可能會有吸引人的範例應用程式,來幫助您完成我 domesticated 程式庫 ︰

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

從 c + + activists 壓力導致我 blush 透過使用 printf,但我無法拒絕其不相符的效能,因此我決定將變成含糊籠統的說實話,我希望透過其他形式的 I/O printf 模組的程式庫。我可以藉由撰寫模組介面啟動 ︰

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

我當然可以只定義的貓而 dog 函數權限模組介面檔案,不過包括它們的運作也一樣。模組宣告會告知編譯器,以下是模組的一部分,但這不表示後續宣告會匯出模組介面的一部分。到目前為止,此模組不會匯出任何項目,除非 stdio.h 標頭的 animals.h 包含剛好匯出項目本身。我甚至可以藉由包含 stdio.h 模組宣告之前防堵的。因此如果此模組介面實際上沒有宣告任何公用名稱,如何著手進行關於匯出供他人使用的項目? 我要使用匯出的關鍵字。這 — 和模組和匯入關鍵字,都只新增我需要考慮以下事項的 c + + 語言的項目。這是專門針對這項新語言功能的美麗的簡易性。

為開始,我可以將匯出的貓和 dog 函數。這牽涉到更新 animals.h 標頭和開始兩個宣告,以匯出規範,如下所示 ︰

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 選項來指示編譯器只編譯,但程式碼的連結。在這個階段,它徒勞無功的舉動,讓連結器嘗試建立可執行檔。[模組] 選項會指示編譯器產生包含描述介面和實作的一種二進位格式中的模組的中繼資料的檔案。此中繼資料不是電腦程式碼中,但 c + + 語言的二進位表示法建構而不是。不過,還有不原始程式碼,狀況良好且不正確,視您如何看待它。很好,因為它應該改善建置效率,因為可能會匯入模組的應用程式不需要重新剖析程式碼。相反地,這也表示,不一定是任何原始程式碼的 Visual Studio 和 IntelliSense 引擎,以視覺化方式檢視和剖析等傳統工具。這表示該 Visual Studio 和其他工具,必須接受教導,學習如何對和視覺化程式碼模組中。好消息是以開放式格式儲存的程式碼或模組內的中繼資料,並可更新工具來處理它。

在我們繼續應用程式現在可以匯入模組,而不是程式庫標頭直接 ︰

C:\modules> type app.cpp
import animals;
int main()
{
  dog();
  cat();
}

匯入宣告會指示編譯器以尋找相符的模組介面檔案。它可以再使用,以及任何其他包含可能會出現在應用程式中,若要解決的狗 」 和 「 cat 函式。幸好動物模組匯出的一組 furry 函式,可以使用相同的模組命令列選項重新編譯應用程式 ︰

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

請注意這次我讓編譯器呼叫連結器,因為現在實際要產生可執行檔。因為匯入關鍵字還不是官方仍然需要實驗模組選項。此外,連結器也需要產生已編譯的模組物件檔案。這一次提示的事實,新的二進位格式,其中包含模組的中繼資料不實際上 「 程式碼,」,而只是匯出的宣告、 函式、 類別、 範本的描述,依此類推。在您要實際建置之應用程式使用模組的時間點,您仍然需要物件檔案,以便執行其工作組合的程式碼的可執行檔的連結器。如果一切順利,我現在有可執行檔,就像任何其他可以執行 — 最終結果是與原始的應用程式使用僅限標頭的程式庫。換句話說,模組不是 DLL。

現在,我剛好處理相當大的程式庫,並加入每個宣告匯出的想法完全不很吸引人。幸運的是,匯出宣告可以匯出不只是函式。其中一個選項是匯出一大堆具有一對括號,宣告,如下所示 ︰

C:\modules> type animals.h
#pragma once
#include <stdio.h>
export
{
  inline void dog()
  {
    printf("woof\n");
  }
  inline void cat()
  {
    printf("meow\n");
  }
}

這不會引入新的範圍,只是用來群組匯出任何所包含的宣告。當然,沒有自我 respecting c + + 程式設計人員可以撰寫一大堆宣告在全域範圍的程式庫。而是更有可能我 animals.h 標頭宣告狗和貓命名空間和命名空間內的函式為可匯出整個很簡單 ︰

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");
  }
}

從僅限標頭的程式庫移到模組的另一個微妙的好處是介面的,應用程式可以不小心不再依賴 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
  {
    // Sadly, this is exported, as well
  }
  // This is exported
}

所幸,做為 [圖 2 所示,使得 animals::impl 命名空間宣告分開,同時保留文件庫的命名空間的結構,您可以重新排列程式碼。

[圖 2 並保留程式庫的命名空間的結構

C:\modules> type animals.h
#pragma once
#include <stdio.h>
namespace animals
{
  namespace impl
  {
    // This is *not* exported -- yay!
  }
}
export namespace animals
{
  // This is exported
}

現在,我們需要實作巢狀命名空間定義 Visual c + +,它會變得相當查看有點美化且輕鬆地進行大量的巢狀命名空間的程式庫管理員 ︰

C:\modules> type animals.h
#pragma once
#include <stdio.h>
namespace animals::impl
{
  // This is *not* exported -- yay
}
export namespace animals
{
  // This is exported
}

希望這項功能會在 Visual c + + Update 3 到達。手指交叉 ! 按照現況,animals.h 標頭會中斷現有的應用程式,只要包含標頭以及或許還不能支援模組的編譯器使用建置。如果您需要支援程式庫的現有使用者時速度很慢將它們轉換為模組,您可以透過使用可怕的前置處理器平滑項目轉換期間。這並不理想。許多較新的 c + + 語言功能,包括模組的設計是為了讓 c + + 程式設計沒有擬真越來越多的巨集。儘管如此之前 c + + 17 中實際登陸模組,而開發人員可使用商業的實作,, 我可以使用有點前置處理器詐讓動物媒體櫃做為僅限標頭的程式庫和模組。在我 animals.h 標頭中,我可以有條件地 ANIMALS_EXPORT 巨集定義為並用來之前我想要匯出如果這是模組的任何命名空間 (請參閱 [圖 3)。

[圖 3 建立程式庫做為僅限標頭的程式庫和做為模組

C:\modules> type animals.h
#pragma once
#include <stdio.h>
#ifndef ANIMALS_EXPORT
#define ANIMALS_EXPORT
#endif
namespace animals { namespace impl {
// Please don't look here
}}
ANIMALS_EXPORT namespace animals {
// This is all yours
}

現在所有開發人員熟悉的模組,或缺乏適當的實作中,可以只包含 animals.h 標頭,並使用就像任何其他僅限標頭的文件庫。不過,可以更新模組介面,定義 ANIMALS_EXPORT,如此這個相同的標頭可能會產生一組匯出的宣告,如下所示 ︰

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

如同許多 c + + 開發人員現在,我不喜歡巨集,而且會而居住的世界不加括號。同樣的這是很有用的技巧與轉換模組的程式庫。最棒的而包含 animals.h 標頭的應用程式則會看到良性巨集,它將不會是可見完全只需匯入模組的人。巨集之前建立的模組的中繼資料清除,如此一來,會永遠不會把應用程式或程式庫和模組,也許可以使用它。

模組是 c + + 的 [歡迎使用新增和我期待編譯器的未來更新商業完整支援。現在,您可以嘗試以及我們,我們的 c + + 標準往前推的模組系統與 c + +。您可以進一步了解模組所讀取的技術規格 (goo.gl/Eyp6EB) 或觀賞提供 Gabriel Dos Reis CppCon 在去年的談一談 (youtu.be/RwdQA0pGWa4)。

共同常式

雖然共同常式,先前稱為可繼續函式,就已經存在得較長 Visual c + + 中,繼續感到興奮的 c + + 語言支援,則為 true coroutine 潛在 — 與深層的原本在 c 的堆疊為基礎的語言設計我在想要寫入的內容,因為它 dawned 我,我撰寫文章不只一個,但至少四個有關本主題為 MSDN Magazine。我建議您開始使用最新的發行項在 2015 年 10 月期 (goo.gl/zpP0AO),其中您將介紹 Visual c + + 2015 所提供的共同常式支援。而不是 rehash 共同常式的優點,讓我們進一步向下鑽研到其中一點。共同常式的 c + + 17 所採用的挑戰之一是標準化委員會不喜歡他們可以提供自動類型推斷的概念。可以由編譯器推算 coroutine 的類型,使開發人員不需要考慮以下事項 ︰ 什麼型別可能是 ︰

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

編譯器以上無法產生適當的 coroutine 型別,說這啟發而由 c + + 14 函式可以有推算其傳回型別所述 ︰

auto get_number()
{
  return 123;
}

同樣的標準化委員會尚無法知道如何與此概念延伸以共同常式。問題是 c + + 標準程式庫並不是提供適合的候選項目。最接近的近似值是笨拙 std:: future,它通常繁重的實作與它非常實用的設計。它也不會協助太多的非同步資料流所產生的產生值的共同常式,而非只以非同步方式傳回單一值。因此,如果編譯器無法提供的類型和 c + + 標準程式庫不提供適合的型別,我需要查看仔細查看這實際上的運作方式若要讓任何使用共同常式的進度。假設我有下列空的 awaitable 型別 ︰

struct await_nothing
{
  bool await_ready() noexcept
  {
    return true;
  }
  void await_suspend(std::experimental::coroutine_handle<>) noexcept
  {}
  void await_resume() noexcept
  {}
};

它沒有任何作用,但讓我能夠藉由在其上等候建構 coroutine:

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

同樣地,如果我不能依賴編譯器自動 deducing coroutine 的傳回型別,我選擇不使用 std:: future,接著如何可能會定義此 coroutine 類別範本嗎?

template <typename T>
struct coroutine;

我已經執行這個發行項的空間不足,因為我們只看傳回 nothing,coroutine 範例或 void。以下是特製化 ︰

template <>
struct coroutine<void>
{
};

第一件事,編譯器會尋找 promise_type coroutine 的傳回型別。還有其他方法可以呼叫這,特別是如果您需要 retrofit coroutine 支援現有的程式庫,但是因為我在撰寫 coroutine 類別樣板我可以只宣告了 ︰

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

接下來,編譯器會尋找 return_void 函式的 coroutine 承諾,至少不傳回值的共同常式 ︰

struct promise_type
{
  void return_void()
  {}
};

雖然 return_void 不需要採取任何動作,它可以當做不同的實作 coroutine 的邏輯結果已準備好進行檢查的狀態變更的信號。編譯器也會尋找一對 initial_suspend 和 final_suspend 函式 ︰

struct promise_type
{
  void return_void()
  {}
  bool initial_suspend()
  {
    return false;
  }
  bool final_suspend()
  {
    return true;
  }
};

編譯器會使用這些屬性插入 coroutine 是否開始暫止狀態 coroutine 以及是否要暫停之前完成 coroutine 告訴排程器的一些初始和最終程式碼。這對函式實際上可傳回 awaitable 型別,以便有效編譯器無法等候兩者,如下所示 ︰

coroutine<void> hello_world()
{
  coroutine<void>::promise_type & promise = ...;
  await promise.initial_suspend();
  await await_nothing{};
  printf("hello world\n");
  await promise.final_suspend();
}

指出要等候,因此,插入暫停點,取決於您嘗試達成。特別是,如果您需要查詢 coroutine 其完成,您會想要確保是最終的暫止;否則,您有機會來查詢任何承諾所擷取的值之前,會終結 coroutine。

編譯器會尋找接下來就是如何取得承諾 coroutine 物件 ︰

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

編譯器可確保 promise_type 配置 coroutine 框架的一部分。然後,必須產生 coroutine 從這項承諾的傳回類型的方法。此內容會接著取得傳回給呼叫者。此處我必須仰賴極低階的協助程式類別所提供的編譯器呼叫 coroutine_handle 和目前提供 std::experimental 命名空間中。Coroutine_handle 代表單一引動過程的 coroutine;因此,我可以將此控制代碼儲存為我 coroutine 類別樣板的成員 ︰

template <>
struct coroutine<void>
{
  // ...
  coroutine_handle<promise_type> handle { nullptr };
};

我會初始化,表示 coroutine 目前不在執行中,但是我也可以新增的建構函式明確地將控制代碼關聯新建構 coroutine nullptr 控制代碼 ︰

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

Coroutine 框架有點像堆疊框架,但是會動態配置的資源,並必須終結,所以我的自然使用解構函式 ︰

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

我也應該刪除複製作業,並允許移動語意,但是您能夠意會就好。我現在可以實作 promise_type get_return_object 函式做為 coroutine 物件的處理站 ︰

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

我現在應該有足夠的編譯器會產生 coroutine 開始到生命。在這裡,同樣地,是 coroutine 後面接著一個簡單的主要函數 ︰

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

我還沒有做任何處理 hello_world 的結果,您尚未執行此程式會導致要呼叫的 printf 和熟悉的訊息列印到主控台。表示實際完成 coroutine 嗎? 也我可以詢問 coroutine 該問題 ︰

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

這次我不是開始使用 coroutine 但詢問是否完成,而且有足夠 ︰

hello world
done: yes

您應該記得 promise_type initial_suspend 函式傳回 false,因此 coroutine 本身不會開始暫止的存留期。還記得,不可以是引入暫停點,因此,傳回 true,await_nothing 的 await_ready 函式。最終結果是實際上會同步完成因為我給它,否則沒有理由 coroutine。優點是,編譯器能夠最佳化以同步方式運作的共同常式,並套用所有屬於相同的最佳化,讓程式碼的速度很快直線。不過,這不是非常令人興奮,所以讓我們加入一些 suspense 或至少部分暫停點。這可能是變更 await_nothing 類型一律暫止,即使它沒有一樣簡單 ︰

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

在此情況下,編譯器會看到,此 awaitable 物件不會準備好傳回給呼叫端才能繼續。現在如果我回到我的簡單的 hello world 應用程式 ︰

int main()
{
  hello_world();
}

我會在失望尋找此程式不會列印出來的任何項目。理由應該很明顯 ︰ Printf 和呼叫端擁有 coroutine 物件在呼叫之前暫止 coroutine 不讓它繼續。當然,繼續 coroutine 很簡單,只要呼叫控制代碼提供繼續函式 ︰

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

現在 hello_world 函式會再次傳回而不需要呼叫 printf,但繼續函式會導致完成 coroutine。若要進一步說明,我可以使用控制代碼的 done 的方法之前和之後繼續執行,,如下所示 ︰

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");
}

結果會清楚地顯示呼叫端和 coroutine 之間的互動 ︰

done: no
hello world
done: yes

這可能是非常實用,特別是在內嵌缺少複雜 OS 排程器和執行緒,因為我就可以輕鬆地撰寫輕量型合作多工作業系統的系統 ︰

while (!routine.handle.done())
{
  routine.handle.resume();
  // Do other interesting work ...
}

共同常式不是魔法,也又需要複雜的排程或同步處理邏輯來加以處理。支援具有傳回型別共同常式包括 promise_type return_void 函式取代 return_value 函式可接受的值,並將它儲存在承諾。Coroutine 完成時,呼叫端可以再擷取的值。產生的值資料流的共同常式需要類似的 yield_value 函式上 promise_type,但除此之外,基本上相同。編譯器提供共同常式是相當簡單的攔截程序尚未不可思議的彈性。我已經皮毛,只在此一簡要概觀,但我希望這可讓您瞭解這個令人讚嘆的新語言功能。

Gor Nishanov,另一個開發人員在 Microsoft,c + + 小組會繼續發送到最終的標準化共同常式。他即使正在加入 Clang 編譯器共同常式的支援 ! 您可以進一步了解共同常式所讀取的技術規格 (goo.gl/9UDeoa) 或觀賞提供 Nishanov CppCon 在去年的談一談 (youtu.be/_fu0gx-xseY)。James McNellis 也演說上共同常式在會議的 c + + (youtu.be/YYtzQ355_Co)。

沒有使用 microsoft 的 c + +,甚至發生。我們正在新增新的 c + + 語言功能,包括從 c + + 14 變數可讓您定義變數的一系列的範本 (goo.gl/1LbDJ2)。Neil MacIntosh 正著手提議的新 c + + 標準程式庫的界限安全字串和順序的檢視。您可以閱讀 span <> 和在 string_span goo.gl/zS2Kaugoo.gl/4w6ayn, ,且即使實作可用兩個 (GitHub.com/Microsoft/GSL)。

在後端,我最近發現 c + + 最佳化工具是很聰明,比我想像談到最佳化離開 strlen 和 wcslen 呼叫與字串常值時呼叫。即使是保管的密碼不特別新。新功能是 Visual c + + 最後會實作 「 完整空基底最佳化,以卻為高於十。套用至所有直接空基底類別中零位移配置版面的類別會 __declspec(empty_bases)。這不是預設值,因為它需要將這類重大變更,編譯器的主要版本更新,並仍有一些 c + + 標準程式庫類型,但假設舊的版面配置。儘管如此,程式庫開發人員可以最後善用此最佳化。現代 c + + Windows 執行階段 (moderncpp.com) 特別有幫助從這也是實際上為什麼為什麼這項功能最後加入至編譯器。我有提到在 2015 年 12 月期,我最近加入了 Windows 團隊在 Microsoft Windows 執行階段為基礎建置新的語言投影 moderncpp.com ,這也可協助 microsoft 推播向前 c + +。別搞錯了,Microsoft 會認真看待 c + +。


Kenny Kerr是 Microsoft 的 Windows 小組的軟體工程師。在他的部落格 kennykerr.ca ,您可以關注他的 Twitter: @kennykerr

感謝下列 Microsoft 技術專家人員檢閱這份文件 ︰ Andrew Pardoe