Pointer ke anggota data kelas “:: *”

243

Saya menemukan potongan kode aneh ini yang mengkompilasi dengan baik:

class Car
{
    public:
    int speed;
};

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

Mengapa C ++ memiliki pointer ini ke anggota data non-statis kelas? Apa gunanya pointer aneh ini dalam kode nyata?

Ashwin Nanjappa
sumber
Di sinilah saya menemukannya, membingungkan saya juga ... tetapi masuk akal sekarang: stackoverflow.com/a/982941/211160
HostileFork mengatakan jangan percaya SE

Jawaban:

190

Ini adalah "penunjuk ke anggota" - kode berikut menggambarkan penggunaannya:

#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;
}

Seperti mengapa Anda ingin melakukan itu, baik itu memberi Anda tingkat tipuan lain yang dapat memecahkan beberapa masalah rumit. Tapi jujur ​​saja, saya belum pernah menggunakannya dalam kode saya sendiri.

Sunting: Saya tidak bisa memikirkan penggunaan yang meyakinkan untuk pointer ke data anggota. Pointer ke fungsi anggota dapat digunakan dalam arsitektur pluggable, tetapi sekali lagi menghasilkan contoh di ruang kecil mengalahkan saya. Berikut ini adalah yang terbaik (belum diuji) coba - fungsi Terapkan yang akan melakukan beberapa pemrosesan pra & pasca sebelum menerapkan fungsi anggota yang dipilih pengguna ke objek:

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

Tanda kurung c->*funcdiperlukan karena ->*operator memiliki prioritas lebih rendah daripada operator fungsi panggilan.

Oktalis
sumber
3
Bisakah Anda menunjukkan contoh situasi rumit di mana ini berguna? Terima kasih.
Ashwin Nanjappa
Saya punya contoh menggunakan pointer-to-member di kelas Traits di jawaban SO lainnya .
Mike DeSimone
Contohnya adalah menulis kelas tipe "callback" untuk beberapa sistem berbasis acara. Sistem berlangganan acara CEGUI UI, misalnya, mengambil panggilan balik templated yang menyimpan pointer ke fungsi anggota yang Anda pilih, sehingga Anda dapat menentukan metode untuk menangani acara tersebut.
Benji XVI
2
Ada contoh yang cukup keren dari pointer-to- data -member penggunaan dalam fungsi templat dalam kode ini
alveko
3
Saya baru-baru ini menggunakan pointer ke anggota data dalam kerangka serialisasi. Objek marshaller statis diinisialisasi dengan daftar pembungkus yang berisi pointer ke anggota data serializable. Prototipe awal dari kode ini.
Alexey Biryukov
79

Ini adalah contoh paling sederhana yang dapat saya pikirkan yang menyampaikan kasus-kasus langka di mana fitur ini berkaitan:

#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;
}

Yang perlu diperhatikan di sini adalah pointer yang diteruskan ke count_fruit. Ini menghemat Anda harus menulis fungsi count_apples dan count_oranges terpisah.

John McFarlane
sumber
3
Bukankah seharusnya &bowls.applesdan &bowls.oranges? &bowl::applesdan &bowl::orangestidak menunjuk apa pun.
Dan Nissenbaum
19
&bowl::applesdan &bowl::orangestidak menunjuk ke anggota suatu objek ; mereka menunjuk anggota kelas . Mereka perlu dikombinasikan dengan pointer ke objek yang sebenarnya sebelum mereka menunjuk ke sesuatu. Kombinasi itu dicapai dengan ->*operator.
John McFarlane
58

Aplikasi lain adalah daftar yang mengganggu. Tipe elemen dapat memberi tahu daftar apa pointer berikutnya / prev. Jadi daftar ini tidak menggunakan nama yang dikodekan tetapi masih dapat menggunakan pointer yang ada:

// 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);
}
Johannes Schaub - litb
sumber
Jika ini benar-benar daftar tertaut, bukankah Anda menginginkan sesuatu seperti ini: void add (E * e) {e -> * next_ptr = head; head = e; } ??
eeeeaaii
4
@ ya saya sarankan Anda untuk membaca tentang parameter referensi. Apa yang saya lakukan pada dasarnya setara dengan apa yang Anda lakukan.
Johannes Schaub - litb
+1 untuk contoh kode Anda, tetapi saya tidak melihat adanya keharusan untuk menggunakan pointer-to-member, contoh lain?
Alcott
3
@Alcott: Anda dapat menerapkannya pada struktur lain yang mirip daftar tertaut di mana penunjuk berikutnya tidak disebutkan next.
icktoofay
41

Inilah contoh dunia nyata yang saya kerjakan saat ini, dari pemrosesan sinyal / sistem kontrol:

Misalkan Anda memiliki beberapa struktur yang mewakili data yang Anda kumpulkan:

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

Sekarang anggaplah Anda memasukkan mereka ke dalam vektor:

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

Sekarang anggaplah Anda ingin menghitung beberapa fungsi (katakanlah mean) dari salah satu variabel pada rentang sampel, dan Anda ingin memasukkan perhitungan rata-rata ini ke dalam suatu fungsi. Pointer-ke-anggota memudahkan:

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);

Catatan Diedit 2016/08/05 untuk pendekatan fungsi-templat yang lebih ringkas

Dan, tentu saja, Anda bisa templat untuk menghitung rata-rata untuk forward-iterator dan semua tipe nilai yang mendukung penambahan dengan dirinya sendiri dan pembagian dengan 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);

EDIT - Kode di atas memiliki implikasi kinerja

Anda harus mencatat, karena saya segera menemukan, bahwa kode di atas memiliki beberapa implikasi kinerja yang serius. Ringkasannya adalah bahwa jika Anda menghitung statistik ringkasan pada deret waktu, atau menghitung FFT dll, maka Anda harus menyimpan nilai untuk setiap variabel yang bersebelahan dalam memori. Jika tidak, mengulangi seri akan menyebabkan cache hilang untuk setiap nilai yang diambil.

Pertimbangkan kinerja kode ini:

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;

Pada banyak arsitektur, satu instance dari Sampleakan mengisi baris cache. Jadi pada setiap iterasi loop, satu sampel akan ditarik dari memori ke dalam cache. 4 byte dari garis cache akan digunakan dan sisanya dibuang, dan iterasi berikutnya akan menghasilkan cache lain yang hilang, akses memori dan sebagainya.

Jauh lebih baik untuk melakukan ini:

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;

Sekarang ketika nilai x pertama dimuat dari memori, tiga berikutnya juga akan dimuat ke dalam cache (seandainya penyelarasan yang sesuai), artinya Anda tidak perlu nilai apa pun dimuat untuk tiga iterasi berikutnya.

Algoritme di atas dapat ditingkatkan sedikit lebih jauh melalui penggunaan instruksi SIMD pada misalnya arsitektur SSE2. Namun, ini bekerja jauh lebih baik jika semua nilai bersebelahan dalam memori dan Anda dapat menggunakan instruksi tunggal untuk memuat empat sampel bersama-sama (lebih banyak di versi SSE nanti).

YMMV - desain struktur data Anda agar sesuai dengan algoritma Anda.

Tom
sumber
Ini luar biasa. Saya akan mengimplementasikan sesuatu yang sangat mirip, dan sekarang saya tidak perlu mencari tahu sintaks yang aneh! Terima kasih!
Nicu Stiurca
Ini jawaban terbaik. Bagian double Sample::*itu kuncinya!
Eyal
37

Anda nanti dapat mengakses anggota ini, dalam hal apa pun :

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

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

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

  return 0;
}

Perhatikan bahwa Anda perlu sebuah instance untuk memanggilnya, sehingga tidak berfungsi seperti delegasi.
Ini jarang digunakan, saya membutuhkannya mungkin sekali atau dua kali selama bertahun-tahun.

Biasanya menggunakan antarmuka (yaitu kelas dasar murni dalam C ++) adalah pilihan desain yang lebih baik.

Peterchen
sumber
Tapi tentunya ini hanya praktik buruk? harus melakukan sesuatu seperti youcar.setspeed (mycar.getpspeed)
thecoshman
9
@thecoshman: sepenuhnya tergantung - menyembunyikan anggota data di belakang metode set / get bukanlah enkapsulasi dan hanya upaya milkmaids pada abstraksi antarmuka. Dalam banyak skenario, "denormalization" kepada anggota publik adalah pilihan yang masuk akal. Tetapi diskusi itu mungkin melebihi batas dari fungsi komentar.
peterchen
4
+1 untuk menunjukkan, jika saya mengerti dengan benar, bahwa ini adalah pointer ke anggota dari instance apa pun, dan bukan pointer ke nilai spesifik dari satu instance, yang merupakan bagian yang saya benar-benar hilang.
johnbakers
@Fellowshee Anda mengerti dengan benar :) (menekankan bahwa dalam jawabannya).
peterchen
26

IBM memiliki beberapa dokumentasi lebih lanjut tentang cara menggunakan ini. Secara singkat, Anda menggunakan pointer sebagai offset ke dalam kelas. Anda tidak dapat menggunakan petunjuk ini selain dari kelas yang mereka maksud, jadi:

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

Tampaknya sedikit tidak jelas, tetapi satu aplikasi yang mungkin adalah jika Anda mencoba menulis kode untuk deserialisasi data generik menjadi banyak jenis objek yang berbeda, dan kode Anda perlu menangani jenis objek yang sama sekali tidak diketahui (misalnya, kode Anda adalah di perpustakaan, dan objek tempat Anda deserialize dibuat oleh pengguna perpustakaan Anda). Pointer anggota memberi Anda cara umum, semi-terbaca untuk merujuk ke offset anggota data individu, tanpa harus menggunakan kekosongan * tipuan tipuan cara Anda mungkin untuk C struct.

Letakkan
sumber
Bisakah Anda membagikan contoh potongan kode di mana konstruk ini berguna? Terima kasih.
Ashwin Nanjappa
2
Saya saat ini melakukan banyak hal karena melakukan beberapa pekerjaan DCOM dan menggunakan kelas sumber daya terkelola yang melibatkan melakukan sedikit pekerjaan sebelum setiap panggilan, dan menggunakan anggota data untuk perwakilan internal untuk mengirim ke com, ditambah templating, membuat banyak kode pelat ketel jauh lebih kecil
Dan
19

Itu memungkinkan untuk mengikat variabel dan fungsi anggota secara seragam. Berikut ini adalah contoh dengan kelas Mobil Anda. Penggunaan yang lebih umum akan mengikat std::pair::firstdan ::secondsaat digunakan dalam algoritma STL dan Tingkatkan pada peta.

#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;
}
Alex B
sumber
11

Anda dapat menggunakan larik pointer untuk data anggota (homogen) untuk mengaktifkan antarmuka ganda, bernama anggota (iexdata) dan array-subscript (yaitu 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;
}
Functastic
sumber
Saya lebih sering melihat ini diimplementasikan menggunakan serikat anonim termasuk bidang array v [3] karena itu menghindari tipuan, tapi tetap pintar, dan berpotensi berguna untuk bidang yang tidak berdekatan.
Dwayne Robinson
2
@DwayneRobinson tetapi menggunakan huruf uniontipe-to dengan cara itu tidak diizinkan oleh standar karena memunculkan berbagai bentuk perilaku yang tidak terdefinisi ... sedangkan jawaban ini ok.
underscore_d
Itu adalah contoh yang rapi tetapi operator [] dapat ditulis ulang tanpa pointer-ke-komponen: float *component[] = { &x, &y, &z }; return *component[idx];Yaitu, pointer-ke-komponen tampaknya tidak memiliki tujuan selain kebingungan.
tobi_s
2

Salah satu cara saya menggunakannya adalah jika saya memiliki dua implementasi tentang bagaimana melakukan sesuatu di kelas dan saya ingin memilih satu pada saat run-time tanpa harus terus menerus melalui pernyataan if yaitu

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;
};

Jelas ini hanya berguna secara praktis jika Anda merasa kode sedang dipalu sehingga pernyataan if memperlambat hal-hal yang dilakukan misalnya. jauh di dalam nyali dari beberapa algoritma intensif di suatu tempat. Saya masih berpikir itu lebih elegan daripada pernyataan if bahkan dalam situasi di mana tidak memiliki penggunaan praktis tapi itu hanya pendapat saya.

Penyanyi
sumber
Pada dasarnya, Anda dapat mencapai hal yang sama dengan abstrak Algorithmdan dua kelas turunan, misalnya, AlgorithmAdan AlgorithmB. Dalam kasus seperti itu, kedua algoritma dipisahkan dengan baik dan dipastikan akan diuji secara independen.
shycha
2

Pointer ke kelas bukan pointer nyata ; kelas adalah konstruk logis dan tidak memiliki keberadaan fisik dalam memori, namun, ketika Anda membangun pointer ke anggota kelas, ia memberikan offset ke objek kelas anggota di mana anggota dapat ditemukan; Ini memberikan kesimpulan penting: Karena anggota statis tidak terkait dengan objek apa pun maka penunjuk ke anggota TIDAK BISA menunjuk ke anggota statis (data atau fungsi) apa pun Pertimbangkan hal berikut:

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;
}

Sumber: Referensi Lengkap C ++ - Herbert Schildt 4th Edition

Arijit Dey
sumber
0

Saya pikir Anda hanya ingin melakukan ini jika data anggota cukup besar (misalnya, objek dari kelas yang cukup besar), dan Anda memiliki beberapa rutin eksternal yang hanya bekerja pada referensi ke objek dari kelas itu. Anda tidak ingin menyalin objek anggota, jadi ini memungkinkan Anda menyebarkannya.

Andrew Jaffe
sumber
0

Berikut adalah contoh di mana penunjuk ke anggota data dapat bermanfaat:

#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
}
prestokeys
sumber
0

Misalkan Anda memiliki struktur. Di dalam struktur itu adalah * semacam nama * dua variabel dari jenis yang sama tetapi dengan makna yang berbeda

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

Oke, sekarang katakanlah Anda memiliki banyak foos dalam sebuah wadah:

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

Oke, sekarang anggaplah Anda memuat data dari sumber yang berbeda, tetapi data disajikan dengan cara yang sama (misalnya, Anda memerlukan metode penguraian yang sama).

Anda dapat melakukan sesuatu seperti ini:

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;
}

Pada titik ini, panggilan readValues()akan mengembalikan sebuah wadah dengan serentetan "input-a" dan "input-b"; semua kunci akan ada, dan foos dengan memiliki a atau b atau keduanya.

inetknght
sumber
0

Hanya untuk menambahkan beberapa use case untuk jawaban @ anon's & @ Oktalist, berikut adalah bahan bacaan yang bagus tentang fungsi pointer-to-anggota dan data pointer-ke-anggota.

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

Dragonly
sumber
tautannya sudah mati. Karena itulah jawaban tautan saja tidak diharapkan di sini. Setidaknya rangkum konten tautan, jika tidak, jawaban Anda menjadi tidak valid ketika tautan membusuk
phuclv