1. 程式人生 > >Visual Studio 2017 中的 C++ 一致性改進

Visual Studio 2017 中的 C++ 一致性改進

工程遷移至VC2017時產生一些新的warning,可參見本文解決!

新語言功能

編譯器支援通用 constexpr 和聚合的 NSDMI,現具有 C++14 標準版中的全部新增功能。 請注意,編譯器仍缺少 C++11 和 C++98 標準版中的一些功能。 請參閱 Visual C++ 語言合規性中顯示編譯器當前狀態的表。

C++11:

在更多庫中支援表示式 SFINAE - Visual C++ 編譯器持續改進對錶達式 SFINAE 的支援,SFINAE 是模板引數推導和替換所必需的,其中 decltype 和 constexpr 表示式可能顯示為模板引數。 有關詳細資訊,請參閱 Visual Studio 2017 RC 中的表示式 SFINAE 改進之處

C++ 14:

用於聚合的 NSDMI - 聚合是一個數組或類,不具有使用者提供的建構函式、專用或受保護的非靜態資料成員、基類,也不具有虛擬函式。 從 C++14 開始,聚合可能包含成員初始值設定項。 有關詳細資訊,請參閱 Member initializers and aggregates(成員初始值設定項和聚合)。

constexpr - 現在允許宣告為 constexpr 的表示式包含某些種類的宣告:if 和 switch 語句、loop 語句,以及某些物件的突變,這些物件的生命期開始時間處於 constexpr 表示式計算範圍內。 此外,不再需要 constexpr 非靜態成員函式為隱式 const。 有關詳細資訊,請參閱

Relaxing constraints on constexpr functions(放鬆對 constexpr 函式的約束)。

C++17:

Terse static_assert(適用於 /std:c++latest)在 C++17 中,static_assert 的訊息引數是可選的。 有關詳細資訊,請參閱 Extending static_assert, v2(擴充套件 static_assert, v2)。

[[fallthrough]] 屬性(適用於 /std:c++latest)[[fallthrough]] 屬性可以在 switch 語句的上下文中用作對預期發生貫穿行為的編譯器的提示。 這可防止編譯器在這類情況下發出警告。 有關詳細資訊,請參閱

Wording for [[fallthrough]] attribute([[fallthrough]] 屬性的用詞)。

通用的基於範圍的 for 迴圈(不需要編譯器開關)基於範圍的 for 迴圈不再需要 begin() 和 end() 返回相同型別的物件。 這使得 end() 能夠返回類似於 Ranges-V3 方案中定義的 ranges 所使用的那種 sentinel 物件。 有關詳細資訊,請參閱 Generalizing the Range-Based For Loop(通用化基於範圍的 for 迴圈)和 range-v3 library on GitHub(GitHub 上的 range-v3 庫)。

有關 Visual Studio 2015 Update 3 中一致性改進的完整列表,請參閱 Visual C++ What's New 2003 through 2015(Visual C++ 2003 至 2015 中的新增功能)。

Bug 修復

複製列表初始化

Visual Studio 2017 使用並非在 Visual Studio 2015 中捕獲的初始值設定項列表正確引發與物件建立相關的編譯器錯誤,並可能導致故障或未定義的執行時行為。 根據 N4594 13.3.1.7p1,在複製列表初始化中,編譯器需要考慮用於過載決策的顯式建構函式,但是如果實際選擇了該過載,則必須引發錯誤。

以下兩個示例在 Visual Studio 2015 中編譯,但在 Visual Studio 2017 中不編譯。

Copy C++
struct A
{
    explicit A(int) {} 
    A(double) {}
};

int main()
{
    A a1 = { 1 }; // error C3445: copy-list-initialization of 'A' cannot use an explicit constructor
    const A& a2 = { 1 }; // error C2440: 'initializing': cannot convert from 'int' to 'const A &'

}

為更正此錯誤,應使用直接初始化:

Copy C++
A a1{ 1 };
const A& a2{ 1 };

在 Visual Studio 2015 中,編譯器以與常規復制初始化相同的方式錯誤地處理複製列表初始化;它只考慮將轉換建構函式用於過載決策。 在以下示例中,Visual Studio 2015 選擇 MyInt(23),但 Visual Studio 2017 正確引發錯誤。

Copy C++
// From http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#1228
struct MyStore {
       explicit MyStore(int initialCapacity);
};

struct MyInt {
       MyInt(int i);
};

struct Printer {
       void operator()(MyStore const& s);
       void operator()(MyInt const& i);
};

void f() {
       Printer p;
       p({ 23 }); // C3066: there are multiple ways that an object of this type can be called with these arguments
}

此示例與上一個示例類似,但引發了不同的錯誤。 它在 Visual Studio 2015 中成功,在 Visual Studio 2017 中失敗 (C2668)。

Copy C++
struct A {
    explicit A(int) {}
};

struct B {
    B(int) {}
};

void f(const A&) {}
void f(const B&) {}

int main()
{
    f({ 1 }); // error C2668: 'f': ambiguous call to overloaded function
}

棄用的的 Typedef

Visual Studio 2017 現在針對在類或結構中宣告的已棄用 Typedef 發出正確警告。 下面的示例在 Visual Studio 2015 中編譯,且未收到警告,但在 Visual Studio 2017 中則引發 C4996。

Copy C++
struct A 
{
    // also for __declspec(deprecated) 
    [[deprecated]] typedef int inttype;
};

int main()
{
    A::inttype a = 0; // C4996 'A::inttype': was declared deprecated
}

constexpr

當條件性運算的左側運算元在 constexpr 上下文中無效時,Visual Studio 2017 會正確引發錯誤。 下列程式碼在 Visual Studio 2015 中進行編譯,但不在 Visual Studio 2017 中進行編譯:

Copy C++
template<int N>
struct array 
{
       int size() const { return N; }
};

constexpr bool f(const array<1> &arr)
{
       return arr.size() == 10 || arr.size() == 11; // error starting in Visual Studio 2017
}

要更正錯誤,可將 array::size() 函式宣告為 constexpr 或從 f 中刪除 constexpr 限定符。

傳遞給 variadic 函式的類型別

在 Visual Studio 2017 中,傳遞給 variadic 函式(如 printf)的類或結構必須完全可複製。 傳遞此類物件時,編譯器只是執行按位複製,不會呼叫建構函式或解構函式。

Copy C++
#include <atomic>
#include <memory>
#include <stdio.h>

int main()
{
    std::atomic<int> i(0);
    printf("%i\n", i); // error C4839: non-standard use of class 'std::atomic<int>'
                        // as an argument to a variadic function
                        // note: the constructor and destructor will not be called; 
                        // a bitwise copy of the class will be passed as the argument
                        // error C2280: 'std::atomic<int>::atomic(const std::atomic<int> &)':
                        // attempting to reference a deleted function

    struct S {
        S(int i) : i(i) {}
        S(const S& other) : i(other.i) {}
        operator int() { return i; }
    private:
        int i;
    } s(0);
    printf("%i\n", s); // warning C4840 : non-portable use of class 'main::S'
                      // as an argument to a variadic function
}

若要更正錯誤,可呼叫一種成員函式,該函式返回可完全複製的型別,

Copy C++
    std::atomic<int> i(0);
    printf("%i\n", i.load());

或者執行靜態強制轉換,以在傳遞物件之前將其進行轉換:

Copy C++
    struct S {/* as before */} s(0);
    printf("%i\n", static_cast<int>(s))

對於使用 CStringW 生成和管理的字串,提供的 ‘operator LPCWSTR()’ 應該用來將 CStringW 物件強制轉換為格式字串所需的 C 指標。

Copy C++
CStringW str1;
CStringW str2;
str1.Format(… , static_cast<LPCWSTR>(str2));

類構造中的 cv 限定符

在 Visual Studio 2015 中,編譯器有時會在通過建構函式呼叫生成類物件時錯誤地忽略 cv 限定符。 這可能會導致故障或意外的執行時行為。 以下示例在 Visual Studio 2015 中編譯,但在 Visual Studio 2017 中引發了編譯器錯誤:

Copy C++
struct S 
{
    S(int);
    operator int();
};

int i = (const S)0; // error C2440

若要更正此錯誤,將運算子 int() 宣告為 const。

對模板中限定名稱的訪問檢查

早期版本的編譯器不對某些模板上下文中的限定名稱執行訪問檢查。 這可能會干擾預期的 SFINAE 行為,在這一行為中,預期由於名稱的不可訪問性,替換會失敗。 這可能會導致在執行時發生故障或意外行為,因為編譯器錯誤地呼叫了運算子的錯誤過載。 在 Visual Studio 2017 中,引發了編譯器錯誤。 具體錯誤可能會有所不同,但通常是“C2672 找不到匹配的過載函式”。 下列程式碼在 Visual Studio 2015 中進行編譯,但在 Visual Studio 2017 中引發錯誤:

Copy C++
#include <type_traits>

template <class T> class S {
       typedef typename T type;
};

template <class T, std::enable_if<std::is_integral<typename S<T>::type>::value, T> * = 0>
bool f(T x);

int main()
{
       f(10); // C2672: No matching overloaded function found. 
}

缺少的模板引數列表

在 Visual Studio 2015 及更早版本中,當模板出現在模板引數列表中(例如作為預設模板引數或非型別模板引數的一部分)時,編譯器不會診斷缺少的模板引數列表。 這可能導致不可預知的行為,包括編譯器故障或意外的執行時行為。 下列程式碼在 Visual Studio 2015 中進行編譯,但在 Visual Studio 2017 中引發錯誤。

Copy C++
template <class T> class ListNode;
template <class T> using ListNodeMember = ListNode<T> T::*;
template <class T, ListNodeMember M> class ListHead; // C2955: 'ListNodeMember': use of alias 
                                                     // template requires template argument list

// correct:  template <class T, ListNodeMember<T> M> class ListHead;  

表示式 SFINAE

為了支援表示式 SFINAE,編譯器現在會在宣告模板而不是例項化模板時分析 decltype 引數。 因此,如果在 decltype 引數中找到非依賴專用化,則它不會被推遲到例項化時間,而會被立即處理,並且將在此時診斷產生的所有錯誤。

以下示例顯示了在宣告時引發的這類編譯器錯誤:

Copy C++
#include <utility>
template <class T, class ReturnT, class... ArgsT> class IsCallable
{
public:
       struct BadType {};
       template <class U>
       static decltype(std::declval<T>()(std::declval<ArgsT>()...)) Test(int); //C2064. Should be declval<U>
       template <class U>
       static BadType Test(...);
       static constexpr bool value = std::is_convertible<decltype(Test<T>(0)), ReturnT>::value;
};

constexpr bool test1 = IsCallable<int(), int>::value;
static_assert(test1, "PASS1");
constexpr bool test2 = !IsCallable<int*, int>::value;
static_assert(test2, "PASS2");

在匿名名稱空間內宣告的類

根據 C++ 標準,在匿名名稱空間內部宣告的類具有內部連結,因此不能匯出。 在 Visual Studio 2015 及更早版本中,此規則不是強制執行的。 在 Visual Studio 2017 中,部分強制執行此規則。 下面的示例在 Visual Studio 2017 中引發錯誤:“錯誤 C2201: 'const anonymous namespace'::S1::vftable'': 必須具有外部連結才能匯出/匯入。”

Copy C++
namespace
{
    struct __declspec(dllexport) S1 { virtual void f() {} }; //C2201
}

在匿名名稱空間內宣告的類

根據 C++ 標準,在匿名名稱空間內部宣告的類具有內部連結,因此不能匯出。 在 Visual Studio 2015 及更早版本中,此規則不是強制執行的。 在 Visual Studio 2017 中,部分強制執行此規則。 下面的示例在 Visual Studio 2017 中引發錯誤:“錯誤 C2201: 'const anonymous namespace'::S1::vftable'': 必須具有外部連結才能匯出/匯入。”

Copy C++
struct __declspec(dllexport) S1 { virtual void f() {} }; //C2201

值類成員的預設初始值設定項 (C++/CLI)

在 Visual Studio 2015 及更早版本中,編譯器允許(但會忽略)值類成員的預設成員初始值設定項。 值類的預設初始化始終對成員執行零初始化;不允許使用預設建構函式。 在 Visual Studio 2017 中,預設成員初始值設定項引發編譯器錯誤,如下例所示:

Copy C++
value struct V
{
       int i = 0; // error C3446: 'V::i': a default member initializer  
                  // is not allowed for a member of a value class
};

預設索引器 (C++/CLI)

在 Visual Studio 2015 及更早版本中,編譯器在某些情況下將預設屬性誤識別為預設索引器。 有可能通過使用識別符號“default”訪問該屬性來解決這個問題。 在 C++11 中將預設值引入為關鍵字後,解決方法本身會出現問題。 因此,在 Visual Studio 2017 中,需要解決方法的 Bug 都已修復,現在將“default”用於訪問類的預設屬性時,編譯器會引發錯誤。

Copy C++
//class1.cs

using System.Reflection;
using System.Runtime.InteropServices;

namespace ClassLibrary1
{
    [DefaultMember("Value")]
    public class Class1
    {
        public int Value
        {
            // using attribute on the return type triggers the compiler bug
            [return: MarshalAs(UnmanagedType.I4)]
            get;
        }
    }
    [DefaultMember("Value")]
    public class Class2
    {
        public int Value
        {
            get;
        }
    }
}


// code.cpp
#using "class1.dll"

void f(ClassLibrary1::Class1 ^r1, ClassLibrary1::Class2 ^r2)
{
       r1->Value; // error
       r1->default;
       r2->Value;
       r2->default; // error
}

在 Visual Studio 2017 中,可以通過屬性名稱同時訪問兩個值屬性:

Copy C++
#using "class1.dll"

void f(ClassLibrary1::Class1 ^r1, ClassLibrary1::Class2 ^r2)
{
       r1->Value;
       r2->Value;
}