2.1 普通函數的參數中的auto
??? 從c++14起,lambda可以使用auto占位符聲明或者定義參數:???
auto printColl = [] (const auto& coll) // generic lambda{ for (const auto& elem : coll) {std::cout << elem << '\n';}}
只要支持Lambda 內部的操作,占位符允許傳遞任何類型的參數:
std::vector coll{1, 2, 4, 5};...printColl(coll); // compiles the lambda for vector<int>printColl(std::string{"hello"}); // compiles the lambda for std::string
從C++20起,我們可以使用auto占位符給所有函數(包括成員函數和運算符):
void printColl(const auto& coll) // generic function
{for (const auto& elem : coll) {std::cout << elem << '\n';}
}
這樣的聲明是僅僅像聲明函數或者定義了如下一個模板:
template<typename T>
void printColl(const T& coll) // equivalent generic function
{for (const auto& elem : coll) {std::cout << elem << '\n';}
}
由于唯一的區別是不使用模板參數T。因此,這個特性也稱為縮寫函數模板語法。
因為帶有auto的函數是函數模板,所以使用函數模板的所有規則都適用。如果在不同的編譯單元中分別調用,
那么auto參數函數的實現不能在cpp文件中,應該放到hpp文件中定義,以便在多個CPP文件中使用,并且不需要聲明為inline函數,因為模板函數總是inline的。
此外,還可以顯式指定模板參數:
void print(auto val)
{std::cout << val << '\n';
}print(64); // val has type intprint<char>(64); // val has type char
2.1.1 成員函數的auto參數
使用這個特性可以定義成員函數:
class MyType {
...
void assign(const auto& newVal);
};
等價于:
class MyType {
...
template<typename T>
void assign(const T& newVal);
};
然而,需要注意的是,模板不能在函數內部聲明。因此,通過這個特性,你不能在函數內部局部定義類或數據結構。
void foo()
{
struct Data {
void mem(auto); // ERROR can’t declare templates insides functions
};
}
2.2? auto的使用
使用auto帶來的好處和方便:auto的延遲類型檢查。
2.2.1 使用auto進行延遲類型檢查
??? 對于使用auto參數,實現具有循環依賴的代碼會更加容易。
??? 例如,考慮兩個使用其他類對象的類。要使用另一個類的對象,您需要其類型的定義;僅進行前向聲明是不夠的(除非只聲明引用或指針)。
class C2; // forward declarationclass C1 {
public:void foo(const C2& c2) const // OK{c2.print(); // ERROR: C2 is incomplete type}void print() const;
};class C2 {
public:void foo(const C1& c1) const{c1.print(); // OK}void print() const;
};
盡管您可以在類定義中實現C2::foo(),但您無法實現C1::foo(),因為為了檢查c2.print()的調用是否有效,編譯器需要C2類的定義。在上述代碼中,當C1的foo()函數調用c2.print()時,由于C2類的定義仍然是不完整的,編譯器無法確定該調用的有效性。因此,這將導致編譯錯誤。
因此,你必須在聲明兩個類的結構之后實現C2::foo():
#include <iostream>class C2;? // forward declarationclass C1
{
public:void foo(const C2& c2) const;void print() const { std::cout << "C1::print" << std::endl;};
};class C2
{
public:void foo(const auto& c1) const{c1.print(); // OK}void print() const { std::cout << "C2::print" << std::endl;};
};inline void C1::foo(const C2& c2) const // implementation (inline if in header)
{c2.print(); // OK
}int main(void)
{C1 c1;C2 c2;c1.foo(c2);c2.foo(c1);return 0;}
由于泛型函數在調用時會檢查泛型參數的成員,因此通過使用auto,您可以簡單地實現以下內容:
#include <iostream>class C1
{
public://template<typename C> void foo(const C& c2) constvoid foo(const auto& c2) const{c2.print(); // OK}void print() const { std::cout << "C1::print" << std::endl;};};class C2
{
public:void foo(const C1& c1) const{c1.print(); // OK}void print() const { std::cout << "C2::print" << std::endl;};
};int main(void)
{C1 c1;C2 c2;c1.foo(c2);c2.foo(c1);??return 0;
}
這并不是什么新鮮事物。當將C1::foo()聲明為成員函數模板時,您將獲得相同的效果。然而,使用auto可以更容易地實現這一點。
請注意,使用auto允許調用者傳遞任意類型的參數,只要該類型提供一個名為print()的成員函數。如果您不希望如此,可以使用標準概念std::same_as來限制僅針對C2類型的參數使用該成員函數:
#include <concepts>class C2;
class C1
{
public:void foo(const std::same_as<C2> auto& c2) const{c2.print(); // OK}void print() const;
};
...
對于概念而言,不完整類型也可以正常工作。這樣,使用std::same_as概念可以確保只有參數類型為C2時才能使用該成員函數。
2.2.2 auto參數函數與lambda的對比
auto參數函數不同于lambda。例如,不能傳遞一個沒有指定具體類型給泛型參數auto的函數:
bool lessByNameFunc(const auto& c1, const auto& c2) { // sorting criterion
??? return c1.getName() < c2.getName(); // compare by name
}
...
std::sort(persons.begin(), persons.end(), lessByNameFunc); // ERROR: can’t deduce type of parameters in sorting criterion
lessByNameFunc函數等價于:
template<typename T1, typename T2>
bool lessByName(const T1& c1, const T1& c2) { // sorting criterion
??? return c1.getName() < c2.getName(); // compare by name
}
由于未直接調用函數模板,編譯器無法在編譯階段將模板參數推導出。因此,必須顯式指定模板參數:
std::sort(persons.begin(), persons.end(),
lessByName<Customer, Customer>); // OK
使用lambda的時候,在傳遞lambda時不必指定模板參數的參數類型:
lessByNameLambda = [] (const auto& c1, const auto& c2) { // sorting criterion
??? return c1.getName() < c2.getName(); // compare by name
};
...
std::sort(persons.begin(), persons.end(), lessByNameLambda); // OK
原因在于lambda是一個沒有通用類型的對象。只有將該對象用作函數時才是通用的。
另一方面,顯式指定(簡寫)函數模板參數會更容易一些。
- 只需在函數名后面傳遞指定的類型即可
????????? void printFunc(const auto& arg) {
?????????????? ...
????????? }
????????? printFunc<std::string>("hello"); // call function template compiled for std::string
對于泛型lambda,由于泛型lambda是一個具有泛型函數調用運算符operator()的函數對象。我們必須按照如下去做:要顯式指定模板參數,你需要將其作為參數傳遞給 operator():
auto printFunc = [] (const auto& arg) {
...
};
printFunc.operator()<std::string>("hello"); // call lambda compiled for std::string
對于通用lambda,函數調用運算符operator()是通用的。因此,您需要將所需的類型作為參數傳遞給operator(),以顯式指定模板參數。
2.3 auto參數其他細節
2.3.1 auto參數的基本約束
使用auto參數去聲明函數遵循的規則與它聲明lambda參數的規則相同:
- 對于用auto聲明的每個參數,函數都有一個隱式模板參數。
- auto參數可以作為參數包void foo(auto… args);相當于
Template<typename … Types>void foo(Types… args);
- decltype(auto)是不允許使用的
?????????
縮寫函數模板仍然可以使用(部分)顯式指定的模板參數進行調用。模板參數的順序與調用參數的順序相同。
例如:
For example:void foo(auto x, auto y)
{...
}foo("hello", 42); // x has type const char*, y has type intfoo<std::string>("hello", 42); // x has type std::string, y has type intfoo<std::string, long>("hello", 42); // x has type std::string, y has type long
2.3.2 結合template和auto參數
簡化的函數模板仍然可以顯式指定模板參數,為占位符類型生成的模板參數可添加到指定參數之后:
template<typename T>
void foo(auto x, T y, auto z)
{
...
}
foo("hello", 42, '?'); // x has type const char*, T and y are int, z is char
foo<long>("hello", 42, '?'); // x has type const char*, T and y are long, z is char
因此,以下聲明是等效的(除了在使用auto的地方沒有類型名稱):
template<typename T>
void foo(auto x, T y, auto z);
等價于
template<typename T, typename T2, typename T3>
void foo(T2 x, T y, T3 z);
正如我們稍后介紹的那樣,通過使用概念作為類型約束,您可以約束占位參數以及模板參數。然后,模板參數可以用于此類限定。
例如,以下聲明確保第二個參數y具有整數類型,并且第三個參數z具有可以轉換為y類型的類型:
template<std::integral T>
void foo(auto x, T y, std::convertible_to<T> auto z)
{
...
}
foo(64, 65, 'c'); // OK, x is int, T and y are int, z is char
foo(64, 65, "c"); // ERROR: "c" cannot be converted to type int (type of 65)
foo<long,short>(64, 65, 'c'); // NOTE: x is short, T and y are long, z is char
請注意,最后一條語句以錯誤的順序指定了參數的類型。
模板參數的順序與預期不符可能會導致難以發現的錯誤。考慮以下示例:
#include <vector>
#include <ranges>void addValInto(const auto& val, auto& coll)
{coll.insert(val);
}template<typename Coll> // Note: different order of template parametersrequires std::ranges::random_access_range<Coll>
void addValInto(const auto& val, Coll& coll)
{coll.push_back(val);
}int main()
{std::vector<int> coll;addValInto(42, coll); // ERROR: ambiguous
}
由于在addValInto的第二個聲明中只對第一個參數使用了auto,導致模板參數的順序不同。根據被C++20接受的http://wg21.link/p2113r0,這意味著重載決議不會? 優先選擇第二個聲明勝過優先選擇第一個聲明,從而導致出現了二義性錯誤。
因此,在混合使用模板參數和auto參數時,請務必小心。理想情況下,使聲明保持一致。
2.3.3 函數參數使用auto的優缺點:
好處:
簡化代碼:使用auto作為參數類型可以減少代碼中的冗余和重復,特別是對于復雜的類型聲明。它可以使代碼更加簡潔、易讀和易于維護。
提高靈活性:auto參數可以適應不同類型的實參,從而提高代碼的靈活性。這對于處理泛型代碼或接受多種類型參數的函數非常有用。
減少錯誤:使用auto作為參數類型可以減少類型推導錯誤的機會。編譯器將根據實參的類型來確定參數的類型,從而降低了手動指定類型時可能出現的錯誤。
后果:
可讀性下降:使用auto作為參數類型會使函數的接口和使用方式不夠明確。閱讀代碼時,無法直接了解參數的預期類型,需要查看函數的實現或上下文來確定。
難以理解:對于復雜的函數或涉及多個參數的函數,使用auto作為參數類型可能會增加代碼的復雜性和難以理解的程度。閱讀和理解函數的功能和使用方式可能需要更多的上下文信息。
潛在的性能影響:使用auto作為參數類型可能會導致一些性能損失。編譯器需要進行類型推導和轉換,可能會引入額外的開銷。在性能敏感的場景中,這可能需要謹慎考慮。
總體而言,使用auto作為參數類型可以簡化代碼并提高靈活性,但也可能降低可讀性和理解性。在決定是否使用auto作為參數類型時,需要權衡其中的利弊,并根據具體情況做出適當的選擇。
#include <vector>
#include <vector>
#include <ranges>
void addValInto(auto& coll, const auto& val)
{
coll.insert(val);
}
template<typename Coll>
requires std::ranges::random_access_range<Coll>
void addValInto(Coll& coll, const auto& val)
{
coll.push_back(val);
}
int main()
{
std::vector<int> coll;
addValInto(coll, 42); // OK, 選擇第二個聲明
}