Getter & Setter 的最佳性能

Best Performance for Getter & Setter

本文关键字:最佳 性能 Setter Getter      更新时间:2023-10-16

最近我使用getter和setter方法进行了一些性能测试。我仍然不确定在c++中使用它们的最佳方法。(使用int等小类型没有区别)

string i;
string GetTest()
{
    return i;
}
void SetTest(string i)
{
    this->i = i; //copy
}

这就是我在java/C#中使用它的方式。没有引用/指针,而且在c++中速度非常慢。对于字符串、矢量和其他"大"类型,如果频繁调用,这是非常缓慢和糟糕的

string i;
const string& GetTest()
{
    return i;
}
void SetTest(const string& i)
{
    this->i = i; //copy
}

目前我正在使用上面的这个版本,它比不使用参数和返回值的引用快得多,而且使用const,我确保它不会被更改。

string* i;
string& GetTest()
{
    return *i;
}
void SetTest(string& i)
{
    this->i = &i;
}

这个版本,使用指针比只使用引用更快,但我认为它更难阅读,如果你想使用类中的值,你必须使用指针。

除了使用引用/指针之外,它还可以通过内联经常被称为getter&setter,因为它们是小函数。

inline GetTest(){....}
inline SetTest(){....}

但如前所述,我仍然不确定在c++中使用它们的最佳方式,或者是否还有其他方式。消除这些歧义是件好事。

编辑:

时隔近4年,以下是我现在对现代C++的建议:(比任何文本都更有帮助)

https://www.youtube.com/watch?v=xnqTKD8uD6451:00

我建议你看完整的视频,赫伯·萨特是一个如此了不起的演说家,一般都会关注CppCon。

[我完全从我之前在Simple C++getter/setters上的回答中复制了第一段]通常使用访问器/赋值器是一种设计气味,即类的公共接口是不完整的。通常来说,你想要一个有用的公共接口,它提供有意义的功能,而不是简单地获取/设置(这比我们在C中使用结构和函数要好一两步)。每次你想写一个赋值函数,很多时候你想先写一个访问器,只需退一步问问自己"我真的需要这个吗?"。

现在,如果您真的想为特定的属性设置getter/setter:前两个版本中的任何一个都是正确的,尽管第二个版本在某些情况下可能会更好。getter应该是constconst string& GetTest() const),这样就可以在const对象上调用它们。

你的第三个版本的指针是非常危险的。虽然前两个具有非常清晰的所有权语义,但第三个没有。您所显示的没有清理的简短片段表明,set函数的调用方必须在类的持续时间内保持字符串的有效性(没有所有权转移)。完全避免这种构造。

最后,如果你只是在学习C++,请考虑C++图书列表中的一本或多本书。最终C++图书指南和列表

编辑:如果你想提高代码的性能,最好的方法是通过优化编译,然后分析并查看热点在哪里。

如果您被限制为C++98/C++03,那么您的第二个代码确实是最佳解决方案。第三段代码很容易出错,而且是非二进制的,如果通过使用指针或更好的智能指针作为参数来解决这个问题,那仍然不理想(而且你还必须处理字符串的内存管理)。

如果您可以使用C++11或C++14,那么对于间接保存其数据并实现移动语义(如std::string)的类型,最佳的setter实现是

void SetTest(std::string a_i) // by value!
{
  i = std::move(a_i); // move assignment
}

这之所以是最优的,是因为对于左值参数,它的成本大约与引用实现一样高(因为std::move防止复制实际数据),但对于右值参数(例如另一个函数的返回值,或者当显式使用std::move时),参数也是移动构造的,因此,在这种情况下,可以避免所有副本,并且与第三个解决方案一样高效,不会出现问题。

请注意,对于大尺寸的类型(如具有许多成员的structs),已经很浅的副本将是昂贵的,因此即使在C++11中,您的第二个版本仍然是此类类型的最佳版本。对于这样的类型,除非类额外管理分配的资源,否则附加的右值版本将不会提高性能。

当然,大多数时候,正确的解决方案是根本没有getter/setter。

你在问题中列出的这三个版本做了三件完全不同的事情,这就是为什么它们以不同的速度运行。在C++中执行字符串getter和setter的"正确"方法是:

std::string _test;
const std::string& GetTest() const
{
    return _test; 
}
void SetTest(const std::string& test)
{
    this->_test = test; //copy
}
//optional, not necessary
void SetTest(std::string&& test)
{
    this->_test = std::move(test);
}

不管其他贡献者怎么说,getter和setter并不一定是糟糕的类设计。它们通常比直接访问成员变量更可取。然而,过度使用Getters/Setters(或直接访问成员变量)是一种代码气味。

您的第一个版本是可靠和正确的(除了Getter不是const,但它要求编译器在各种情况下不必要地复制和销毁字符串:

string GetTest()
{
    return i;
}
void SetTest(string i)
{
    this->i = i; //copy
}
...
// this is slower than the "right" way, because it requires creating a copy of the string and destroying it:
bool isEmpty = something.GetTest().size() != 0; // the "right" way does not create a copy here.
// this is slower than the "right way, because it requires creating a copy of the string and destroying it:
std::string myString("I am your father, Luke.");
something.SetTest(myString); // this copies "myString", then calls SetTest with the copy, then destroys the copy

你建议的第三个版本应该不惜一切代价避免,因为它是一个脆弱的设计,肯定会导致意想不到的错误:

string* i;
string& GetTest()
{
    return *i;
}
void SetTest(string& i)
{
    this->i = &i;
}
// this modifies the string after the setter
std::string* myString = new std::string("I am your father, Luke.");
someting.SetTest(*myString);
myString->assign("Oranges");
// now this is true: someting.GetTest() == "Oranges"
delete myString;
// now this is undefined behaviour: someting.GetTest() == "Oranges"

如果您真的真的需要类似java/c#的行为,那么执行第三个版本的"正确"方法如下:

std::shared_ptr<const std::string> _test;
const std::shared_ptr<const std::string>& GetTest() const
{
    return _test; 
}
void SetTest(const std::shared_ptr<const std::string>& test)
{
    this->_test = test;
}
void SetTest(std::shared_ptr<const std::string>&& test)
{
    this->_test = std::move(test);
}

如果将指向数据的指针存储在getter和setter中,则所有权和对象生存期将出现重大问题。Jave和C#对此有自己的处理方法。在C++中,您需要使用各种各样的智能指针(在这种情况下可能是shared_ptr,但最好的指南是测试您获得的实际性能)。

对于大多数C++编译器,无论对它们做什么,小的get/set方法都将内联。如果在头文件中定义实现,那么它们肯定会内联!

唯一的例外是,如果它们被构建到共享库中(这很难内联到调用方的代码中),但即使这样,您也可以在现代编译器中使用链接时内联进行编译,然后也会在那里内联!

即使是复杂的类型,也有一种叫做"返回值优化"(或RVO)的东西,即您的返回类型被编译为使用与调用方使用的类型相同的存储,即整个副本被优化掉。

诀窍是让编译器通过保持代码简单来完成它的任务。测试代码的性能是可能的,但我认为你无论如何都会在其他领域调整性能。

PS。getter和setter。。。它们的设计很糟糕。不要鼓励他们。