Почему std::invoke работает для данных в классе?
Почему std::invoke работает ещё и для указателей на данные в классе, а не только для указаталей на функции-члены? Разве логически он не должен только какие-то функциональные объекты вызывать?
Это сказано в документации, а ещё иллюстрируется примером:
#include <functional>
struct A {
int x = 42;
double y = 42.;
} a;
int main() {
std::invoke(&A::x, a);
std::invoke(&A::y, a);
}
Ожидаемое мной поведение в такой ситуации — ошибка компиляции.
Ответы (2 шт):
Ну а почему нет? Это удобно.
Вот пример:
struct A
{
bool is_dead = false;
};
std::vector<A> vec = ...;
std::erase_if(vec, &A::is_dead);
Разве не удобно? Но этот код не компилируется, потому что std::erase_if (и вообще стандартные алгоритмы) не используют std::invoke.
Свободный перевод ответа участника @Nicol Bolas для этого вопроса.
При разработке стандартной библиотеки C++11 множество фич было взято из библиотек Boost. Для ответа на этот вопрос интересны следующие:
bindfunctionreference_wrappermem_fn
Все они, в той или иной степени, относятся к какому-то вызываемому объекту, который может принимать некоторое количество параметров и возвращать определённое значение. Раз так, они пытаются обращаться с вызываемыми объектами единообразно. С++11, когда адаптировал эти фичи, ввёл концепцию INVOKE вместе с правилами идеальной передачи аргументов, для того чтобы они могли схожим образом работать с вызываемыми объектами.
Единственная цель mem_fn состоит в преобразовании указателя на член в объект, вызываемый с помощью (). Для указателей на функции-члены очевидным при таком вызове является вызов функции-члена, на который этот указатель указывает, передавая экземпляр класса и необходимые аргументы. Для указателей на переменные класса наиболее очевидным поведением является возвращение значения этой переменной в переданном экземпляре класса.
Возможность превращать указатель на переменную в классе в унарную функцию, которая возвращает значение этой переменной, достаточно полезна сама по себе. Вы можете использовать что-то вроде std::transform, передавая mem_fn для указателя на переменную-член, чтобы сгенерировать последовательность значений, которые обращаются к этой переменной. Ranges, добавленные в C++20, делают её ещё более полезной, так как вы можете создавать трансформированные ranges, манипулируя последовательностями объектов, используя только указатель на член.
Дело в том, что мы хотим, чтобы это работало независимо от типов этих объектов. Если объект оказался вызываемым, то mem_fn должен быть в состоянии обращаться к этому объекту, как и к любому другому объекту. Тот факт, что он оказался вызываемым, не имеет значения для цели mem_fn.
Но boost::function и boost::bind могут принимать указатели на члены. Поведение в случае такой передачи основано на boost::mem_fn. Поэтому, если mem_fn трактует указатели на переменные-члены как унарные функции, возвращающие значения этих переменных, то и boost::function с boost::bind должны трактовать их таким же образом.
Когда C++11 собрал всё это в одну обобщённую концепцию, такое поведение перешло в INVOKE.
Таким образом, с этимологической стороны, именно поэтому INVOKE работает так: потому что все указанные фичи должны обращаться с вызываемыми объектами одинаково и весь смысл mem_fn относительно указателей на переменные-члены состоит в обращении с ними как с унарными функциями, возвращающими значения этих переменных. Поэтому именно так все остальные должны обрабатывать эти указатели.
И разве это не хорошо? Разве это неверное поведение? Действительно бы вы хотели, чтобы указатели на члены класса вели себя совершенно по-разному в зависимости от того, указывают ли они на вызываемые объекты или нет? Это бы сделало невозможным написание обобщённого кода, который принимает такие указатели и производит операции над последовательностями объектов с помощью них. Как бы вы смогли обобщённо обращаться к указателям, не зная заранее, получите ли вы ссылку на объект или вызовете его?