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 int
print<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 declaration
class 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 declaration
class 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) const
void 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 int
foo<std::string>("hello", 42); // x has type std::string, y has type int
foo<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 parameters
requires 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, 选择第二个声明
}