Зачем мне оператор .* в С++?

Недавно я узнал, что оператор .* (и тесно связанный с ним оператор ->*) существует в C++. (См. этот вопрос.)

На первый взгляд кажется аккуратным, но зачем мне это вообще нужно? Два ответа на связанный вопрос предоставили надуманные примеры, которые выиграли бы от прямого вызова функции.

Если прямой вызов функции неудобен, вместо него можно использовать функциональный объект, например лямбда-функции, которые можно использовать в std::sort. Это устраняет уровень косвенности и, следовательно, будет более производительным, чем использование .*.

В связанном вопросе также упоминается упрощенная версия этого примера:

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
}

В заключение, указатель на функцию-член может быть заменен функциональным объектом, а указатель на переменную-член может быть заменен прямой ссылкой на указанную переменную или объект функции. Это также может повысить эффективность кода из-за меньшего количества косвенных обращений.

Этот вопрос содержит только примеры, подтверждающие мой вывод, поэтому это так не отвечайте на мой вопрос.

Помимо взаимодействия с унаследованным кодом, который использует .* (в котором вообще не было бы выбора), когда, на самом деле, я хотел бы использовать .*?


person Bernard    schedule 28.06.2017    source источник
comment
Возможный дубликат C++: указатель на элемент данных класса   -  person Stargateur    schedule 28.06.2017
comment
может быть, вы думаете о тривиальных примерах и действительно нуждаетесь в сложных примерах, но я полагаю, что это не место для этого   -  person Emiliano    schedule 28.06.2017
comment
@Emiliano Похоже, что все примеры, которые я видел до сих пор, могут быть заменены шаблонным функциональным объектом (который обеспечит повышение скорости за счет меньшего разыменования), например лямбда-функцией для std::sort(), или просто неконстантная ссылка.   -  person Bernard    schedule 28.06.2017
comment
@Stargateur Я обновил свой вопрос, чтобы объяснить, почему ни один из ответов на другой вопрос не является удовлетворительным.   -  person Bernard    schedule 28.06.2017
comment
Это достаточно хороший пример указателя на члены? stackoverflow.com/a/34165367/2104697   -  person Guillaume Racicot    schedule 28.06.2017
comment
@GuillaumeRacicot Нет. Я писал что-то подобное раньше, но я просто добавил третий параметр шаблона в PropertyImpl, который является Callable, принимающим один параметр типа Class и возвращающим ссылку на правильный элемент. В конце концов, вы уже используете шаблоны. Таким образом, тип элемента кортежа инкапсулирует достаточную информацию, чтобы уже найти правильный член, устраняя необходимость хранить указатель на член.   -  person Bernard    schedule 28.06.2017
comment
@Bernard lambda не является constexpr до С++ 17, поэтому вам нужно будет писать функции в другом месте, что приводит к шаблону. И потребуется установщик, поэтому потребуется написать две функции для каждого свойства, которое вы хотели бы иметь, но мой пример — это одна небольшая строка для каждого свойства.   -  person Guillaume Racicot    schedule 28.06.2017
comment
@GuillaumeRacicot Извините, я не совсем понимаю, что вы имеете в виду. В моей реализации я бы сделал что-то вроде property([] (Dog& x) -> decltype(x.barktype) { return x.barktype; });, для чего не потребуется constexpr, потому что параметр x неизвестен во время компиляции. Или даже property([] (auto&& x) -> decltype(std::forward<decltype(x)>(x).barktype) { return std::forward<decltype(x)>(x).barktype; });, но это довольно долго.   -  person Bernard    schedule 28.06.2017
comment
@Bernard Бернард, да, если у вас больше сериализации, основанной на времени выполнения, это применимо. В моем примере свойствами являются constexpr. Поскольку свойства известны во время компиляции, вы можете сериализовать классы в фиксированную структуру, такую ​​как кортеж, универсальным образом. Кроме того, даже если вам это не нужно, я нахожу &Dog::barktype гораздо менее подробным, чем [] (Dog& d) -> decltype(d.barktype) { return d.barktype }   -  person Guillaume Racicot    schedule 28.06.2017
comment
@Bernard Дело в том, что вы можете написать весь код без указателя на элемент. Всегда будет другой способ реализовать вещи без указателя на член. Но иногда указатели на члены значительно упрощают написание, и это оправдывает их использование.   -  person Guillaume Racicot    schedule 28.06.2017


Ответы (4)


Ваш пример слишком тривиален, чтобы быть иллюстративным. Рассмотрим немного более сложный

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 и без раздувания кода?

person AnT    schedule 28.06.2017

Вы можете создавать коллекции указателей на члены и перебирать их. Например.:

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);
    }
}
person David Scarlett    schedule 28.06.2017
comment
Это можно сделать быстрее, используя шаблоны, и для этого не требуется больше кода, чем в вашем примере. См. мою версию здесь. - person Bernard; 28.06.2017
comment
@Bernard: Тем не менее, в общем случае это вызывает раздувание кода. В реальной жизни решение о том, использовать ли параметры времени выполнения (см. выше) или параметры времени компиляции (шаблоны), не является простым. Так что ваш аргумент о том, что с помощью шаблонов можно сделать быстрее, слишком примитивен. Цена шаблонов — это раздувание кода, которое может быть огромным в случае более крупной функции. Правильный дизайн шаблонного кода требует знания того, когда следует переключиться на параметризацию во время выполнения, чтобы подавить ненужное раздувание кода. Когда дело доходит до выбора членов, указатели на члены — это именно тот инструмент параметризации во время выполнения. - person AnT; 28.06.2017

Он используется для реализации 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");
}
person MaxC    schedule 28.06.2017

Допустим, вы хотите написать библиотеку стилей LINQ для C++, которую можно использовать примерно так:

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 принимает строку формата и некоторый указатель на элементы и выполняет форматирование в стиле sprintf любых элементов вектора, которые проходят через операторы where.

Я написал что-то подобное, и есть несколько других, которые делают это немного по-другому (Есть ли библиотека LINQ для C++?). Указатель на член позволяет пользователю библиотеки указывать любые члены своей структуры, которые ему нужны, и библиотеке не нужно ничего знать о том, что они могут делать.

person Jerry Jeremiah    schedule 28.06.2017
comment
Как можно было заставить &Person::last_name == "Smith" компилироваться? Кроме того, не будут ли работать селекторы в виде лямбда-функций, таких как this или std::find_if и std::remove_if (и как дополнительное преимущество, сделать код быстрее)? Кстати, Ranges TS, похоже, идет по тому же пути. - person Bernard; 28.06.2017
comment
@Bernard, функция принимает дерево выражений. Оператор == принимает указатель на элемент и значение и возвращает функтор, который при выполнении делает то, что делает. Но создание дерева выражений — это время компиляции, поэтому оно достаточно быстрое. Существует, может быть, дюжина таких библиотек в стиле LINQ — все они принимают свои параметры немного по-разному, но те, которые работают с массивами структур, используют указатель на элемент для выбора нужного элемента. Как я уже сказал, я сам написал что-то подобное несколько лет назад... - person Jerry Jeremiah; 28.06.2017
comment
@Bernard Бернард Я написал код по памяти, так что, возможно, синтаксис не совсем правильный. Возможно, использование лямбда-выражений было бы намного лучше, но я написал свою версию этой библиотеки LINQ в 2005 году, используя древний компилятор, поэтому вы можете быть правы в том, что СЕЙЧАС нет причин использовать эти функции — возможно, они были полезны только до лямбда-выражений? - person Jerry Jeremiah; 28.06.2017
comment
Во время выполнения будет еще одна косвенная ссылка для вычисления адреса x.last_name, если он указан с x. Возможно, до лямбда-выражений C++11 проблема написания дополнительных классов с определенным operator() перевешивала бы преимущества отсутствия необходимости считывать смещения объекта во время выполнения. - person Bernard; 28.06.2017
comment
@Bernard Основная проблема с отдельным функтором для каждого сравнения, которое вы хотите, заключается в том, что они не являются локальными, и вы не можете видеть, что на самом деле происходит в коде, который помещается на экране. Я могу себе представить такие функции, как last_name_is() и age_less_than() и другие, чтобы вы могли вызывать find_if() и другие, но причина, по которой лямбда-выражения были включены в стандарт, заключалась в том, что деревья выражений были настолько полезны, и все использовали их для лямбда-выражений. Так что вы правы - создание собственного не так хорошо, как использование стандартных лямбда-выражений, а указатель на член, вероятно, не лучшая практика, если у вас новый компилятор. - person Jerry Jeremiah; 28.06.2017