簡體   English   中英

指向 class 數據成員“::*”的指針

[英]Pointer to class data member "::*"

我遇到了這個編譯得很好的奇怪代碼片段:

class Car
{
    public:
    int speed;
};

int main()
{
    int Car::*pSpeed = &Car::speed;
    return 0;
}

為什么C++ 有這個指向 class 的非靜態數據成員的指針? 這個奇怪的指針在實際代碼中有什么用?

它是一個“指向成員的指針”——下面的代碼說明了它的用法:

#include <iostream>
using namespace std;

class Car
{
    public:
    int speed;
};

int main()
{
    int Car::*pSpeed = &Car::speed;

    Car c1;
    c1.speed = 1;       // direct access
    cout << "speed is " << c1.speed << endl;
    c1.*pSpeed = 2;     // access via pointer to member
    cout << "speed is " << c1.speed << endl;
    return 0;
}

至於您為什么要這樣做,它為您提供了另一個層次的間接性,可以解決一些棘手的問題。 但老實說,我從來沒有在自己的代碼中使用過它們。

編輯:我想不出一個令人信服的使用指向成員數據的指針。 指向成員函數的指針可用於可插拔架構中,但再次在狹小的空間中生成示例讓我失望。 以下是我最好的(未經測試)嘗試-應用 function 在將用戶選擇的成員 function 應用於 ZA8CFDE6331BD59EB2AC96F8911C4B6666 之前會進行一些前后處理:

void Apply( SomeClass * c, void (SomeClass::*func)() ) {
    // do hefty pre-call processing
    (c->*func)();  // call user specified function
    // do hefty post-call processing
}

c->*func周圍的括號是必要的,因為->*運算符的優先級低於 function 調用運算符。

這是我能想到的最簡單的示例,它傳達了與此功能相關的罕見情況:

#include <iostream>

class bowl {
public:
    int apples;
    int oranges;
};

int count_fruit(bowl * begin, bowl * end, int bowl::*fruit)
{
    int count = 0;
    for (bowl * iterator = begin; iterator != end; ++ iterator)
        count += iterator->*fruit;
    return count;
}

int main()
{
    bowl bowls[2] = {
        { 1, 2 },
        { 3, 5 }
    };
    std::cout << "I have " << count_fruit(bowls, bowls + 2, & bowl::apples) << " apples\n";
    std::cout << "I have " << count_fruit(bowls, bowls + 2, & bowl::oranges) << " oranges\n";
    return 0;
}

這里要注意的是傳入count_fruit的指針。 這使您不必編寫單獨的 count_apples 和 count_oranges 函數。

另一個應用是侵入式列表。 元素類型可以告訴列表它的下一個/上一個指針是什么。 所以列表不使用硬編碼的名稱,但仍然可以使用現有的指針:

// say this is some existing structure. And we want to use
// a list. We can tell it that the next pointer
// is apple::next.
struct apple {
    int data;
    apple * next;
};

// simple example of a minimal intrusive list. Could specify the
// member pointer as template argument too, if we wanted:
// template<typename E, E *E::*next_ptr>
template<typename E>
struct List {
    List(E *E::*next_ptr):head(0), next_ptr(next_ptr) { }

    void add(E &e) {
        // access its next pointer by the member pointer
        e.*next_ptr = head;
        head = &e;
    }

    E * head;
    E *E::*next_ptr;
};

int main() {
    List<apple> lst(&apple::next);

    apple a;
    lst.add(a);
}

這是我現在正在處理的一個真實示例,來自信號處理/控制系統:

假設您有一些表示您正在收集的數據的結構:

struct Sample {
    time_t time;
    double value1;
    double value2;
    double value3;
};

現在假設您將它們填充到向量中:

std::vector<Sample> samples;
... fill the vector ...

現在假設您想要計算一個變量在一系列樣本中的一些 function(比如平均值),並且您希望將此平均值計算納入 function。 指向成員的指針使它變得容易:

double Mean(std::vector<Sample>::const_iterator begin, 
    std::vector<Sample>::const_iterator end,
    double Sample::* var)
{
    float mean = 0;
    int samples = 0;
    for(; begin != end; begin++) {
        const Sample& s = *begin;
        mean += s.*var;
        samples++;
    }
    mean /= samples;
    return mean;
}

...
double mean = Mean(samples.begin(), samples.end(), &Sample::value2);

注意 2016/08/05 編輯,以獲得更簡潔的模板函數方法

而且,當然,您可以對其進行模板化,以計算任何前向迭代器和任何支持與自身相加和除以 size_t 的值類型的平均值:

template<typename Titer, typename S>
S mean(Titer begin, const Titer& end, S std::iterator_traits<Titer>::value_type::* var) {
    using T = typename std::iterator_traits<Titer>::value_type;
    S sum = 0;
    size_t samples = 0;
    for( ; begin != end ; ++begin ) {
        const T& s = *begin;
        sum += s.*var;
        samples++;
    }
    return sum / samples;
}

struct Sample {
    double x;
}

std::vector<Sample> samples { {1.0}, {2.0}, {3.0} };
double m = mean(samples.begin(), samples.end(), &Sample::x);

編輯 - 上面的代碼具有性能影響

你應該注意到,正如我很快發現的那樣,上面的代碼有一些嚴重的性能影響。 總結是,如果您正在計算時間序列的匯總統計量,或計算 FFT 等,那么您應該將每個變量的值連續存儲在 memory 中。 否則,遍歷該系列將導致每個檢索到的值的緩存未命中。

考慮這段代碼的性能:

struct Sample {
  float w, x, y, z;
};

std::vector<Sample> series = ...;

float sum = 0;
int samples = 0;
for(auto it = series.begin(); it != series.end(); it++) {
  sum += *it.x;
  samples++;
}
float mean = sum / samples;

在許多架構上,一個Sample實例將填充一個緩存行。 因此,在循環的每次迭代中,將從 memory 中提取一個樣本到緩存中。 將使用緩存行中的 4 個字節,並丟棄 rest,下一次迭代將導致另一個緩存未命中,memory 訪問等等。

這樣做要好得多:

struct Samples {
  std::vector<float> w, x, y, z;
};

Samples series = ...;

float sum = 0;
float samples = 0;
for(auto it = series.x.begin(); it != series.x.end(); it++) {
  sum += *it;
  samples++;
}
float mean = sum / samples;

現在,當從 memory 加載第一個 x 值時,接下來的三個也將加載到緩存中(假設合適的對齊方式),這意味着您不需要為接下來的三個迭代加載任何值。

通過在例如 SSE2 架構上使用 SIMD 指令,可以進一步改進上述算法。 但是,如果這些值在 memory 中都是連續的,並且您可以使用一條指令將四個樣本一起加載(在以后的 SSE 版本中更多),則這些工作更好。

YMMV - 設計您的數據結構以適合您的算法。

您可以稍后在任何實例上訪問此成員:

int main()
{    
  int Car::*pSpeed = &Car::speed;    
  Car myCar;
  Car yourCar;

  int mySpeed = myCar.*pSpeed;
  int yourSpeed = yourCar.*pSpeed;

  assert(mySpeed > yourSpeed); // ;-)

  return 0;
}

請注意,您確實需要一個實例來調用它,因此它不像委托那樣工作。
它很少使用,我多年來可能需要它一兩次。

通常使用接口(即 C++ 中的純基礎 class)是更好的設計選擇。

IBM有更多關於如何使用它的文檔。 簡而言之,您將指針用作 class 的偏移量。 除了它們所指的 class 之外,您不能使用這些指針,因此:

  int Car::*pSpeed = &Car::speed;
  Car mycar;
  mycar.*pSpeed = 65;

看起來有點晦澀,但一個可能的應用是,如果您嘗試編寫代碼以將通用數據反序列化為許多不同的 object 類型,並且您的代碼需要處理它完全不知道的 object 類型(例如,您的代碼是在庫中,並且您反序列化的對象是由您的庫的用戶創建的)。 成員指針為您提供了一種通用的、半易讀的方式來引用各個數據成員的偏移量,而不必像 C 結構那樣使用無類型的 void * 技巧。

它使得以統一的方式綁定成員變量和函數成為可能。 以下是您的汽車 class 的示例。 更常見的用法是在 STL 算法和 map 上使用時綁定std::pair::first::second

#include <list>
#include <algorithm>
#include <iostream>
#include <iterator>
#include <boost/lambda/lambda.hpp>
#include <boost/lambda/bind.hpp>


class Car {
public:
    Car(int s): speed(s) {}
    void drive() {
        std::cout << "Driving at " << speed << " km/h" << std::endl;
    }
    int speed;
};

int main() {

    using namespace std;
    using namespace boost::lambda;

    list<Car> l;
    l.push_back(Car(10));
    l.push_back(Car(140));
    l.push_back(Car(130));
    l.push_back(Car(60));

    // Speeding cars
    list<Car> s;

    // Binding a value to a member variable.
    // Find all cars with speed over 60 km/h.
    remove_copy_if(l.begin(), l.end(),
                   back_inserter(s),
                   bind(&Car::speed, _1) <= 60);

    // Binding a value to a member function.
    // Call a function on each car.
    for_each(s.begin(), s.end(), bind(&Car::drive, _1));

    return 0;
}

您可以使用指向(同質)成員數據的指針數組來啟用雙重命名成員(iexdata)和數組下標(即x[idx])接口。

#include <cassert>
#include <cstddef>

struct vector3 {
    float x;
    float y;
    float z;

    float& operator[](std::size_t idx) {
        static float vector3::*component[3] = {
            &vector3::x, &vector3::y, &vector3::z
        };
        return this->*component[idx];
    }
};

int main()
{
    vector3 v = { 0.0f, 1.0f, 2.0f };

    assert(&v[0] == &v.x);
    assert(&v[1] == &v.y);
    assert(&v[2] == &v.z);

    for (std::size_t i = 0; i < 3; ++i) {
        v[i] += 1.0f;
    }

    assert(v.x == 1.0f);
    assert(v.y == 2.0f);
    assert(v.z == 3.0f);

    return 0;
}

指向類的指針不是真正的指針; a class is a logical construct and has no physical existence in memory, however, when you construct a pointer to a member of a class it gives an offset into an object of the member's class where the member can be found; 這給出了一個重要的結論:由於 static 成員不與任何 object 相關聯,因此指向成員的指針不能指向 static 成員(數據或函數)無論如何考慮以下

class x {
public:
    int val;
    x(int i) { val = i;}

    int get_val() { return val; }
    int d_val(int i) {return i+i; }
};

int main() {
    int (x::* data) = &x::val;               //pointer to data member
    int (x::* func)(int) = &x::d_val;        //pointer to function member

    x ob1(1), ob2(2);

    cout <<ob1.*data;
    cout <<ob2.*data;

    cout <<(ob1.*func)(ob1.*data);
    cout <<(ob2.*func)(ob2.*data);


    return 0;
}

資料來源:完整參考 C++ - Herbert Schildt 第 4 版

我使用它的一種方法是,如果我有兩種如何在 class 中執行某些操作的實現,並且我想在運行時選擇一個,而不必通過 if 語句不斷地 go 即

class Algorithm
{
public:
    Algorithm() : m_impFn( &Algorithm::implementationA ) {}
    void frequentlyCalled()
    {
        // Avoid if ( using A ) else if ( using B ) type of thing
        (this->*m_impFn)();
    }
private:
    void implementationA() { /*...*/ }
    void implementationB() { /*...*/ }

    typedef void ( Algorithm::*IMP_FN ) ();
    IMP_FN m_impFn;
};

顯然,如果您覺得代碼被敲打到足以使 if 語句減慢某些事情的速度,例如,這實際上是有用的。 在某處某個密集算法的內部深處。 即使在沒有實際用途的情況下,我仍然認為它比 if 語句更優雅,但這只是我的意見。

這是一個指向數據成員的指針可能有用的示例:

#include <iostream>
#include <list>
#include <string>

template <typename Container, typename T, typename DataPtr>
typename Container::value_type searchByDataMember (const Container& container, const T& t, DataPtr ptr) {
    for (const typename Container::value_type& x : container) {
        if (x->*ptr == t)
            return x;
    }
    return typename Container::value_type{};
}

struct Object {
    int ID, value;
    std::string name;
    Object (int i, int v, const std::string& n) : ID(i), value(v), name(n) {}
};

std::list<Object*> objects { new Object(5,6,"Sam"), new Object(11,7,"Mark"), new Object(9,12,"Rob"),
    new Object(2,11,"Tom"), new Object(15,16,"John") };

int main() {
    const Object* object = searchByDataMember (objects, 11, &Object::value);
    std::cout << object->name << '\n';  // Tom
}

假設你有一個結構。 該結構內部是 * 某種名稱 * 兩個相同類型但含義不同的變量

struct foo {
    std::string a;
    std::string b;
};

好的,現在假設您在容器中有一堆foo

// key: some sort of name, value: a foo instance
std::map<std::string, foo> container;

好的,現在假設您從不同的源加載數據,但數據以相同的方式呈現(例如,您需要相同的解析方法)。

你可以這樣做:

void readDataFromText(std::istream & input, std::map<std::string, foo> & container, std::string foo::*storage) {
    std::string line, name, value;

    // while lines are successfully retrieved
    while (std::getline(input, line)) {
        std::stringstream linestr(line);
        if ( line.empty() ) {
            continue;
        }

        // retrieve name and value
        linestr >> name >> value;

        // store value into correct storage, whichever one is correct
        container[name].*storage = value;
    }
}

std::map<std::string, foo> readValues() {
    std::map<std::string, foo> foos;

    std::ifstream a("input-a");
    readDataFromText(a, foos, &foo::a);
    std::ifstream b("input-b");
    readDataFromText(b, foos, &foo::b);
    return foos;
}

此時,調用readValues()將返回一個“input-a”和“input-b”一致的容器; 所有鍵都將存在,並且 foos 具有 a 或 b 或兩者兼有。

這是一個例子。 我發現了一些單元測試代碼,其中在不同的位置多次實例化了一個巨大的數據結構 class。 它總共占用了數百行。 該 class 的每個數據成員都是 const 的,它們只能在構造函數中設置。 每個實例化一次更改幾個參數,但其中大多數具有默認值。 所以我必須想辦法讓數據成員在單元測試中盡可能優雅地設置,而不改變原來的 class。 我的解決方案是,你猜對了,數據成員指針。

struct HugeDataStructure {
     const int    i = 0;
     const double d = 0.0;
};

struct MutableHugeDataStructure : public HugeDataStructure {
    template<class T>
    auto& ForceSet (const T HugeDataStructure::* target, const T& value) {
        const_cast<T&> (this->*target) = value;
        return *this;
    }
};

#define SET(memberName, value) ForceSet(&HugeDataStructure::memberName, value)

它可以像這樣使用:

MutableHugeDataStructure data;
data.SET(i, 5) .SET(d, 3.5);

指向成員的真實世界示例可能是 std::shared_ptr 的更窄的別名構造函數:

template <typename T>
template <typename U>
shared_ptr<T>::shared_ptr(const shared_ptr<U>, T U::*member);

那個構造函數有什么好處

假設你有一個 struct foo:

struct foo {
    int ival;
    float fval;
};

如果您已將 shared_ptr 賦予 foo,則可以使用該構造函數將 shared_ptr 檢索到其成員 ival 或 fval:

auto foo_shared = std::make_shared<foo>();
auto ival_shared = std::shared_ptr<int>(foo_shared, &foo::ival);

如果想將指針 foo_shared->ival 傳遞給一些需要 shared_ptr 的 function 這將很有用

https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr

指向成員的指針是 C 的offsetof()的 C++ 類型安全等效項,它在stddef.h中定義:兩者都返回信息,其中某個字段位於classstruct中。 雖然offsetof()也可以在 C++ 中與某些足夠簡單的類一起使用,但它在一般情況下會失敗,尤其是對於虛擬基類。 所以指向成員的指針被添加到標准中。 它們還提供了更簡單的語法來引用實際字段:

struct C { int a; int b; } c;
int C::* intptr = &C::a;       // or &C::b, depending on the field wanted
c.*intptr += 1;

比:

struct C { int a; int b; } c;
int intoffset = offsetof(struct C, a);
* (int *) (((char *) (void *) &c) + intoffset) += 1;

至於為什么要使用offsetof() (或指向成員的指針),stackoverflow 的其他地方有很好的答案。 一個例子在這里: C offsetof 宏是如何工作的?

使用指向成員的指針,我們可以編寫這樣的通用代碼

template<typename T, typename U>
struct alpha{
   T U::*p_some_member;
};

struct beta{
   int foo;
};

int main()
{

   beta b{};

   alpha<int, beta> a{&beta::foo};

   b.*(a.p_some_member) = 4;

   return 0;
}

我喜歡*&運算符:

struct X 
{ 
    int a {0}; 
    int *ptr {NULL};

    int &fa() { return a; }
    int *&fptr() { return ptr; }
};

int main(void) 
{
    X x;
    int X::*p1 = &X::a;     // pointer-to-member 'int X::a'. Type of p1 = 'int X::*'
    x.*p1 = 10;

    int *X::*p2 = &X::ptr;  // pointer-to-member-pointer 'int *X::ptr'. Type of p2 = 'int *X::*' 
    x.*p2 = nullptr;
    X *xx;
    xx->*p2 = nullptr;

    int& (X::*p3)() = X::fa; // pointer-to-member-function 'X::fa'. Type of p3 = 'int &(X::*)()'
    (x.*p3)() = 20; 
    (xx->*p3)() = 30;

    int *&(X::*p4)() = X::fptr;  // pointer-to-member-function 'X::fptr'. Type of p4 = 'int *&(X::*)()'
    (x.*p4)() = nullptr; 
    (xx->*p4)() = nullptr;
}

確實,只要成員是公開的,或者 static

只是為@anon 和@Oktalist 的答案添加一些用例,這里有一個關於指針到成員函數和指針到成員數據的很好的閱讀材料。

https://www.dre.vanderbilt.edu/~schmidt/PDF/C++-ptmf4.pdf

我認為您只想在成員數據非常大的情況下執行此操作(例如,另一個相當大的類的 object),並且您有一些外部例程僅適用於對該 class 對象的引用。 您不想復制成員 object,所以這可以讓您傳遞它。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM