为什么我想在C++中使用.*运算符?

10

我最近发现C++中存在.*运算符(和密切相关的->*运算符)。(参见这个问题。)

一开始看起来很不错,但是我何时需要使用它呢?链接问题中的两个答案提供了一些虚构的示例,这些示例将受益于直接函数调用。

在直接函数调用不方便的情况下,可以使用函数对象,例如可能在std::sort中使用的lambda函数。 这将消除一个间接级别,因此比使用.*更有效率。

链接问题还提到了该示例的简化版本:

struct A {
    int a;
    int b;
};

void set_member(A& obj, int A::* ptr, int val){
    obj.*ptr = val;
}

int main()
{
    A obj;
    set_member(obj, &A::b, 5);
    set_member(obj, &A::a, 7);
    // Both members of obj are now assigned
}

不过这样做相当简单(也许甚至更好的做法,因为它更加干净,而且不会不必要地局限于A类成员),可以使用以下方法:

struct A {
    int a;
    int b;
};

void set_me(int& out, int val){
    out = val;
}

int main()
{
    A obj;
    set_me(obj.b, 5);
    set_me(obj.a, 7);
    // Both members of obj are now assigned
}

总之,一个指向成员函数的指针可以被一个函数对象所取代,而一个指向成员变量的指针可以被该变量的直接引用或一个函数对象所取代。这样做可能还会由于少了一次间接寻址而提高代码的效率。

这个问题 只提供了我的结论成立的例子,因此它并没有回答我的问题

除了在与使用.*的遗留代码进行接口时(在这种情况下根本没有选择),我真正想使用.*的情况是什么时候?


1
可能是C++:指向类数据成员的指针的重复问题。 - Stargateur
2
@Stargateur,我更新了我的问题,解释了为什么其他问题中的答案都不令人满意。 - Bernard
2
@GuillaumeRacicot 不是的。我之前写过类似的东西,但我只是在 PropertyImpl 中添加了第三个模板参数,它是一个可调用对象,接受一个类型为 Class 的单一参数,并返回对正确成员的引用。毕竟,你已经在使用模板了。通过这种方式,元组元素的类型已经封装了足够的信息来找到正确的成员,从而消除了存储指向成员的指针的需要。 - Bernard
@Bernard 在 C++17 之前,lambda 表达式不是 constexpr,因此您需要在其他地方编写函数,从而产生样板代码。并且需要一个 setter,因此您需要为每个要拥有的属性编写两个函数,但我的示例只需要每个属性一行小代码。 - Guillaume Racicot
1
@Bernard,关键是,当然你可以写出所有的代码而不使用成员指针。总会有另一种实现方式来避免使用成员指针。但有时,成员指针能够让编写代码变得更加容易,这就证明了它们的用途。 - Guillaume Racicot
显示剩余5条评论
4个回答

6
您可以创建成员指针的集合并对其进行迭代。例如:
struct UserStrings
{
    std::string first_name;
    std::string surname;
    std::string preferred_name;
    std::string address;
};

...

std::array<std::string UserStrings::*, 4> str_cols = { &UserStrings::first_name, &UserStrings::surname, &UserStrings::preferred_name, &UserStrings::address };
std::vector<UserStrings> users = GetUserStrings();

for (auto& user : users)
{
    for (auto& column : str_cols)
    {
        SanitizeForSQLQuery(user.*column);
    }
}

这可以通过使用模板来加快速度,而且不需要比您的示例更多的代码。请查看我的版本 这里 - Bernard
2
@贝尔纳德:然而,在一般情况下,它会导致代码膨胀。在实际应用中,是否使用运行时参数(上述内容)还是编译时参数(模板)并不容易做出决策。因此,你关于“使用模板可以加快速度”的论点过于简单化了。模板的代价就是代码膨胀,在较大函数的情况下可能会非常严重。合适的模板代码设计需要明确何时切换到运行时参数化来避免不必要的代码膨胀。指向成员的指针正是在选择成员时进行运行时参数化的工具。 - AnT stands with Russia

6
您的示例太简单,无法说明问题。考虑一个更复杂的例子。
struct A {
    int a;
    int b;
};

void set_n_members(A objs[], unsigned n, int A::* ptr, int val)
{
  for (unsigned i = 0; i < n; ++i)
     objs[i].*ptr = val;
}

int main()
{
    A objs[100];
    set_n_members(objs, 100, &A::b, 5);
    set_n_members(objs, 100, &A::a, 7);
}

你如何在不使用int A::* ptr及不引入代码膨胀的情况下重写这段代码?


2

它被用于实现std::mem_fn,而std::function则使用了它。

下面的代码展示了在一个简单的Function类实现中如何使用->*运算符。

同样地,你可以使用.*运算符和一个类引用来实现一个成员调用器类。

#include <iostream>

class A
{
public:
    void greet()
    {
        std::cout << "Hello world"<<std::endl;
    }
};

template<typename R, typename ...TArgs>
class Invoker 
{
public:
    virtual R apply(TArgs&& ...args) = 0;
};

template<typename C, typename R, typename ...TArgs>
class MemberInvoker :public Invoker<R, TArgs...>
{
protected:
    C*                          sender;
    R(C::*function)(TArgs ...args);

public:
    MemberInvoker(C* _sender, R(C::*_function)(TArgs ...args))
        :sender(_sender)
        , function(_function)
    {
    }

    virtual R apply(TArgs&& ...args) override
    {
        return (sender->*function)(std::forward<TArgs>(args)...);
    }
};

template<typename T>
class Func
{
};

template<typename R, typename ...TArgs>
class Func<R(TArgs...)>
{
public:
    Invoker<R,TArgs...>* invoker=nullptr;

    template<typename C>
    Func(C* sender, R(C::*function)(TArgs...))
    {
        invoker =new MemberInvoker<C, R, TArgs...>(sender, function);
    }

    R operator()(TArgs&& ...args)
    {
        return  invoker->apply(std::forward<TArgs>(args)...);
    }

    ~Func()
    {
        if (invoker)
        {
            delete invoker;
            invoker = nullptr;
        }
    }
};

int main()
{
    A a;
    Func<void()> greetFunc(&a, &A::greet);
    greetFunc();
    system("PAUSE");
}

0

假设你想为C++编写一个类似于LINQ的库,可以像这样使用:

struct Person
{
    std::string first_name;
    std::string last_name;
    std::string occupation;
    int age;
    int children;
};

std::vector<Person> people = loadPeople();
std::vector<std::string> result = from(people)
     .where(&Person::last_name == "Smith")
     .where(&Person::age > 30)
     .select("%s %s",&Person::first_name,&Person::last_name);
for(std::string person : result) { ... };

在底层,where函数接受一个表达式树,其中包含成员指针(以及其他内容),并应用于每个向量项,寻找匹配的项。 select语句接受格式字符串和一些成员指针,并对通过where语句的任何向量项进行样式格式化。

我已经编写了类似这样的代码,还有其他几种略有不同的方法(是否有适用于C ++的LINQ库?)。 成员指针允许库用户指定他们想要的结构体成员,而库不需要知道它们可能做什么。


“&Person::last_name ==“ Smith” ”怎么可能编译通过?此外,像这样将选择器编写为lambda函数或像std::find_ifstd::remove_if一样是否也可以(并且作为额外的好处,使代码更快)? 顺便说一句,Ranges TS似乎正在走类似的路线。 - Bernard
@Bernard 函数接受一个表达式树。operator==接受成员指针和一个值,并返回一个函数对象,执行时会执行它的操作。但是,表达式树的创建是编译时完成的,因此速度相当快。可能有十几个这样的LINQ样式的库存在 - 它们中的所有库都稍微以不同的方式接受它们的参数,但是那些对结构体数组起作用的库使用指向成员的指针来选择它们想要的成员。就像我说的,我几年前就写过类似的东西... - Jerry Jeremiah
@Bernard 我是凭记忆写的代码,所以语法可能不完全正确。也许使用lambda表达式会更好,但我在2005年使用古老的编译器编写了我的那个LINQ库版本,所以你说现在没有理由再使用这些特性也有道理——也许它们只在lambda表达式出现之前有用? - Jerry Jeremiah
在运行时,当提供给x时,需要多一个间接寻址来计算x.last_name的地址。或许在C++11 lambda出现之前,编写定义了operator()的额外类所带来的麻烦超过了不需要在运行时读取对象偏移量的好处。 - Bernard
1
@Bernard,为每个比较单独创建一个函数对象的主要问题在于它们是非本地的,而且你无法看到适合屏幕的代码中实际发生了什么。我可以想象有像last_name_is()和age_less_than()这样的函数,这样你就可以调用find_if()等函数,但是lambda被放入标准库的原因是因为表达式树非常有用,而且每个人都在使用它们来创建lambda。所以你是正确的——自己编写不如使用标准的lambda,如果你有一个新的编译器,指向成员的指针可能不是最佳实践。 - Jerry Jeremiah

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接