Inisialisasi beberapa anggota kelas konstan menggunakan satu panggilan fungsi C ++

50

Jika saya memiliki dua variabel anggota konstan yang berbeda, yang keduanya perlu diinisialisasi berdasarkan pemanggilan fungsi yang sama, apakah ada cara untuk melakukan ini tanpa memanggil fungsi dua kali?

Misalnya, kelas pecahan di mana pembilang dan penyebutnya konstan.

int gcd(int a, int b); // Greatest Common Divisor
class Fraction {
public:
    // Lets say we want to initialize to a reduced fraction
    Fraction(int a, int b) : numerator(a/gcd(a,b)), denominator(b/gcd(a,b))
    {

    }
private:
    const int numerator, denominator;
};

Ini menghasilkan waktu yang terbuang, karena fungsi GCD disebut dua kali. Anda juga dapat mendefinisikan anggota kelas baru gcd_a_b,, dan pertama-tama menetapkan output gcd ke yang ada dalam daftar penginisialisasi, tetapi kemudian ini akan menyebabkan memori yang terbuang.

Secara umum, apakah ada cara untuk melakukan ini tanpa panggilan fungsi atau memori yang terbuang? Bisakah Anda membuat variabel sementara dalam daftar penginisialisasi? Terima kasih.

Qq0
sumber
5
Apakah Anda memiliki bukti bahwa "fungsi GCD disebut dua kali"? Disebutkan dua kali, tetapi itu tidak sama dengan kode pemancar kompiler yang menyebutnya dua kali. Kompiler dapat menyimpulkan bahwa itu adalah fungsi murni dan menggunakan kembali nilainya pada penyebutan kedua.
Eric Towers
6
@EricTowers: Ya, kompiler terkadang dapat mengatasi masalah dalam praktik untuk beberapa kasus. Tetapi hanya jika mereka dapat melihat definisi (atau beberapa anotasi dalam suatu objek), jika tidak ada cara untuk membuktikannya murni. Anda harus mengkompilasi dengan optimasi tautan-waktu yang diaktifkan, tetapi tidak semua orang melakukannya. Dan fungsinya mungkin di perpustakaan. Atau pertimbangkan kasus fungsi yang memang memiliki efek samping, dan menyebutnya tepat sekali adalah masalah kebenaran?
Peter Cordes
@EricTowers Poin menarik. Saya benar-benar mencoba memeriksanya dengan meletakkan pernyataan cetak di dalam fungsi GCD, tetapi sekarang saya menyadari bahwa itu akan mencegahnya menjadi fungsi murni.
Qq0
@ Qq0: Anda dapat memeriksa dengan melihat kompiler yang dihasilkan asm, misalnya menggunakan explorer compiler Godbolt dengan gcc atau dentang -O3. Tapi mungkin untuk implementasi tes sederhana itu sebenarnya akan sebaris dengan panggilan fungsi. Jika Anda menggunakan __attribute__((const))atau murni pada prototipe tanpa memberikan definisi yang terlihat, itu harus membiarkan GCC atau dentang melakukan eliminasi umum-sub-ekspresi (CSE) antara dua panggilan dengan arg yang sama. Perhatikan bahwa jawaban Drew bekerja bahkan untuk fungsi non-murni sehingga jauh lebih baik dan Anda harus menggunakannya kapan saja func mungkin tidak sebaris.
Peter Cordes
Secara umum, variabel anggota konstanta non-statis sebaiknya dihindari. Salah satu dari sedikit area di mana const semuanya tidak sering berlaku. Misalnya Anda tidak dapat menetapkan objek kelas. Anda dapat mengganti_back menjadi vektor tetapi hanya selama batas kapasitas tidak mengubah ukuran.
dougou

Jawaban:

66

Secara umum, apakah ada cara untuk melakukan ini tanpa panggilan fungsi atau memori yang terbuang?

Iya. Ini dapat dilakukan dengan konstruktor pendelegasian , diperkenalkan pada C ++ 11.

Sebuah konstruktor mendelegasikan adalah cara yang sangat efisien untuk memperoleh nilai sementara yang dibutuhkan untuk pembangunan sebelum setiap variabel anggota diinisialisasi.

int gcd(int a, int b); // Greatest Common Divisor
class Fraction {
public:
    // Call gcd ONCE, and forward the result to another constructor.
    Fraction(int a, int b) : Fraction(a,b,gcd(a,b))
    {
    }
private:
    // This constructor is private, as it is an
    // implementation detail and not part of the public interface.
    Fraction(int a, int b, int g_c_d) : numerator(a/g_c_d), denominator(b/g_c_d)
    {
    }
    const int numerator, denominator;
};
Drew Dormann
sumber
Karena ketertarikan, apakah overhead dari memanggil konstruktor lain menjadi signifikan?
Qq0
1
@ Qq0 Anda dapat mengamati di sini bahwa tidak ada overhead dengan optimalisasi sederhana diaktifkan.
Drew Dormann
2
@ Qq0: C ++ dirancang di sekitar kompiler optimisasi modern. Mereka dapat dengan sepele menguraikan delegasi ini, terutama jika Anda membuatnya terlihat dalam definisi kelas (dalam .h), bahkan jika definisi konstruktor yang sebenarnya tidak terlihat untuk inlining. yaitu gcd()panggilan akan inline ke masing-masing callsite konstruktor, dan menyerahkan hanya callkepada konstruktor pribadi 3-operan.
Peter Cordes
10

Vars anggota diinisialisasi oleh urutan mereka dinyatakan dalam deklerasi kelas, maka Anda dapat melakukan hal berikut (secara matematis)

#include <iostream>
int gcd(int a, int b){return 2;}; // Greatest Common Divisor of (4, 6) just to test
class Fraction {
public:
    // Lets say we want to initialize to a reduced fraction
    Fraction(int a, int b) : numerator{a/gcd(a,b)}, denominator(b/(a/numerator))
    {

    }
//private:
    const int numerator, denominator;//make sure that they are in this order
};
//Test
int main(){
    Fraction f{4,6};
    std::cout << f.numerator << " / " << f.denominator;
}

Tidak perlu memanggil konstruktor lain atau bahkan membuatnya.

asmmo
sumber
6
ok yang bekerja untuk GCD secara khusus, tetapi banyak kasus penggunaan lainnya mungkin tidak dapat menurunkan const 2 dari args dan pertama. Dan seperti yang ditulis ini memiliki satu divisi tambahan yang merupakan kelemahan lain vs ideal yang mungkin tidak dioptimalkan oleh kompiler. GCD mungkin hanya berharga satu divisi, jadi ini hampir seburuk memanggil GCD dua kali. (Dengan asumsi bahwa divisi mendominasi biaya operasi lain, seperti yang sering terjadi pada CPU modern.)
Peter Cordes
@PeterCordes tetapi solusi lainnya memiliki panggilan fungsi tambahan dan mengalokasikan lebih banyak memori instruksi.
asmmo
1
Apakah Anda berbicara tentang konstruktor delegasi Drew? Itu jelas dapat Fraction(a,b,gcd(a,b))mengarahkan delegasi ke penelepon, yang mengarah ke total biaya yang lebih sedikit. Inlining itu lebih mudah dilakukan oleh kompiler daripada membatalkan pembagian tambahan dalam hal ini. Saya tidak mencobanya di godbolt.org tetapi Anda bisa jika Anda penasaran. Gunakan gcc atau dentang -O3seperti yang biasa digunakan build. (C ++ dirancang berdasarkan asumsi kompiler optimisasi modern, maka fitur seperti constexpr)
Peter Cordes
-3

@Drew Dormann memberikan solusi yang mirip dengan yang ada dalam pikiran saya. Karena OP tidak pernah menyebutkan tidak dapat memodifikasi ctor, ini dapat disebut dengan Fraction f {a, b, gcd(a, b)}:

Fraction(int a, int b, int tmp): numerator {a/tmp}, denominator {b/tmp}
{
}

Hanya dengan cara ini tidak ada panggilan kedua ke fungsi, konstruktor atau sebaliknya, jadi tidak ada waktu yang terbuang. Dan itu bukan memori yang sia-sia karena sementara harus dibuat pula, jadi Anda sebaiknya memanfaatkannya. Itu juga menghindari pembagian ekstra.

warga negara yang peduli
sumber
3
Hasil edit Anda membuatnya bahkan tidak menjawab pertanyaan. Sekarang Anda meminta penelepon untuk mengeluarkan arg ke-3? Versi asli Anda menggunakan penugasan di dalam tubuh konstruktor tidak berfungsi const, tetapi setidaknya berfungsi untuk jenis lainnya. Dan pembagian ekstra apa yang Anda "juga" hindari? Maksudmu vs jawaban asmmo?
Peter Cordes
1
Oke, hapus downvote saya sekarang setelah Anda menjelaskan maksud Anda. Tapi ini tampaknya cukup mengerikan, dan mengharuskan Anda untuk secara manual memasukkan beberapa pekerjaan konstruktor ke setiap penelepon. Ini adalah kebalikan dari KERING (jangan ulangi diri Anda sendiri) dan enkapsulasi tanggung jawab kelas / internal. Kebanyakan orang tidak akan menganggap ini sebagai solusi yang dapat diterima. Mengingat bahwa ada cara C ++ 11 untuk melakukan ini dengan bersih, tidak seorang pun boleh melakukan ini kecuali mungkin mereka terjebak dengan versi C ++ yang lebih lama, dan kelas hanya memiliki sedikit panggilan ke konstruktor ini.
Peter Cordes
2
@ aconcernedcitizen: Maksud saya bukan karena alasan kinerja, maksud saya karena alasan kualitas kode. Dengan cara Anda, jika Anda pernah mengubah cara kerja kelas ini secara internal, Anda harus mencari semua panggilan ke konstruktor dan mengubah argumen ke-3 itu. Ekstra itu ,gcd(foo, bar)adalah kode tambahan yang bisa dan karenanya harus difaktorkan dari setiap ruang info di sumber . Itu masalah pemeliharaan / keterbacaan, bukan kinerja. Kompiler kemungkinan besar akan mengikutinya pada waktu kompilasi, yang Anda inginkan untuk kinerja.
Peter Cordes
1
@PeterCordes Anda benar, sekarang saya melihat pikiran saya terpaku pada solusinya, dan saya mengabaikan yang lainnya. Either way, jawabannya tetap, jika hanya untuk mempermalukan. Setiap kali saya ragu tentang hal itu, saya akan tahu ke mana harus mencari.
warga negara yang peduli
1
Juga pertimbangkan kasus Fraction f( x+y, a+b ); Untuk menuliskannya dengan cara Anda, Anda harus menulis BadFraction f( x+y, a+b, gcd(x+y, a+b) );atau menggunakan tmp vars. Atau bahkan lebih buruk, bagaimana jika Anda ingin menulis Fraction f( foo(x), bar(y) );- maka Anda akan memerlukan situs panggilan untuk mendeklarasikan beberapa tmp vars untuk menyimpan nilai kembali, atau memanggil fungsi-fungsi itu lagi dan berharap kompiler CSE mereka pergi, yang mana kami hindari. Apakah Anda ingin men-debug kasus satu pemanggil yang menggabungkan args ke gcdsehingga sebenarnya bukan GCD dari 2 args pertama yang diteruskan ke konstruktor? Tidak? Maka jangan membuat bug itu mungkin.
Peter Cordes