Ініціалізуйте кілька членів постійного класу, використовуючи одну виклик функції C ++


50

Якщо у мене є дві різні змінні постійних членів, які обидві потрібно ініціалізувати на основі одного і того ж виклику функції, чи є спосіб зробити це без виклику функції двічі?

Наприклад, клас дробу, де чисельник і знаменник є постійними.

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

Це призводить до втрати часу, оскільки функція GCD викликається двічі. Ви також можете визначити нового учасника класу gcd_a_bта спершу призначити вихід gcd цьому в списку ініціалізатора, але потім це призведе до марної пам'яті.

Взагалі, чи існує спосіб це зробити без марних викликів функцій чи пам'яті? Чи можете ви створити тимчасові змінні у списку ініціалізатора? Дякую.


5
Чи є у вас докази того, що "функція GCD викликається двічі"? Це згадується двічі, але це не те саме, що компілятор, що випромінює код, який викликає його двічі. Компілятор може зробити висновок, що це чиста функція, і використовувати її значення при другій згадці.
Ерік Тауерс

6
@EricTowers: Так, компілятори іноді можуть вирішити проблему на практиці в деяких випадках. Але лише якщо вони можуть побачити визначення (або якусь анотацію в об’єкті), інакше жодного способу довести, що це чисто. Ви повинні компілювати з увімкненою оптимізацією часу зв’язку, але це не всі. І ця функція може бути в бібліотеці. Або розглядати випадок функції, яка має побічні ефекти, і називати її точно один раз - це питання правильності?
Пітер Кордес

@EricTowers Цікавий момент. Я насправді намагався перевірити це, вставивши оператор друку всередині функції GCD, але тепер я розумію, що це не дозволить йому бути чистою функцією.
Qq0

@ Qq0: Ви можете перевірити, поглянувши на створений ASM компілятор, наприклад, використовуючи провідник компілятора Godbolt з gcc або clang -O3. Але, ймовірно, для будь-якої простої тестової реалізації вона фактично вбудує виклик функції. Якщо ви використовуєте __attribute__((const))чи чисті в прототипі, не надаючи видимого визначення, він повинен дозволити GCC або clang виконувати загальну субекспресію (CSE) між двома дзвінками з однаковою arg. Зауважте, що відповідь Дрю працює навіть для нечистих функцій, тому це набагато краще, і ви повинні використовувати її будь-коли, коли функція може не вбудовуватися.
Пітер Кордес

Як правило, найкраще уникати нестатичних змінних членів const. Одне з небагатьох напрямків, де const все не часто застосовується. Наприклад, ви не можете призначити об'єкти класу. Ви можете перемістити в вектор, але лише до тих пір, поки обмеження ємності не змінюється за розміром.
дог

Відповіді:


67

Взагалі, чи існує спосіб це зробити без марних викликів функцій чи пам'яті?

Так. Це можна зробити за допомогою конструктора делегування , введеного в C ++ 11.

Делегуючий конструктор - це дуже ефективний спосіб отримати тимчасові значення, необхідні для побудови, перш ніж ініціалізувати будь-які змінні.

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

Невже зацікавлення може мати значні витрати на виклик іншого конструктора?
Qq0

1
@ Qq0 Тут можна помітити, що немає накладних витрат із включеними скромними оптимізаціями.
Дрю Дорман

2
@ Qq0: C ++ розроблений навколо сучасних оптимізуючих компіляторів. Вони можуть тривільно вписати це делегування, особливо якщо ви зробите це видимим у визначенні класу (в .h), навіть якщо для вбудованого рядка не видно реального визначення конструктора. тобто gcd()виклик буде вбудований у кожну сторінку виклику конструктора і залишатиме лише a callприватному конструктору 3-операнду.
Пітер Кордес

10

Параметри vars-члена ініціалізуються в порядку, де вони оголошені в декларації класу, отже, ви можете зробити наступне (математично)

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

Не потрібно телефонувати іншим конструкторам або навіть робити їх.


6
добре, що працює для GCD конкретно, але багато інших випадків використання, ймовірно, не можуть отримати 2-й конст з аргументів і перший. І як написано, у цього є один додатковий поділ, який є іншим недоліком проти ідеального, який компілятор може не оптимізувати. GCD може коштувати лише одного поділу, тому це може бути майже таким же погано, як дзвінок GCD два рази. (Якщо припустити, що підрозділ домінує над витратами інших операцій, як це часто робиться в сучасних процесорах.)
Пітер Кордес

@PeterCordes, але інше рішення має додатковий виклик функції та виділяє більше пам'яті інструкцій.
asmmo

1
Ви говорите про делегуючого конструктора Дрю? Це очевидно може вписати Fraction(a,b,gcd(a,b))делегування абоненту, що призведе до менших загальних витрат. Цей компілятор простіше зробити компілятору, ніж скасувати зайвий поділ у цьому. Я не пробував цього на godbolt.org, але ви можете, якщо вам цікаво. Використовуйте gcc або clang, -O3як звичайна збірка. (C ++ розроблений на основі припущення про сучасний оптимізуючий компілятор, звідси такі функції, як constexpr)
Пітер Кордес

-3

@Drew Dormann дав рішення, подібне до того, що я мав на увазі. Оскільки OP ніколи не згадує про неможливість модифікації ctor, це можна викликати за допомогою Fraction f {a, b, gcd(a, b)}:

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

Тільки так немає другого виклику функції, конструктора чи іншим чином, тому це не витрачений час. І це не марно пам'ять, оскільки тимчасово потрібно було б створити тимчасовий, так що ви також можете добре використовувати це. Це також дозволяє уникнути зайвого поділу.


3
Якщо ваша редакція не відповідає навіть на запитання. Тепер ви вимагаєте, щоб абонент передав 3-й аргумент? Ваша оригінальна версія з використанням призначення всередині корпусу конструктора не працює const, але принаймні працює для інших типів. І якого додаткового поділу ви "також" уникаєте? Ви маєте на увазі відповідь asmmo?
Пітер Кордес

1
Гаразд, видалив мою суботу зараз, коли ти пояснив свою думку. Але це здається досить очевидно жахливим і вимагає, щоб ви вручну вклали частину роботи конструктора в кожен абонент. Це протилежність DRY (не повторюйте себе) та інкапсуляція відповідальності / внутрішніх справ класу. Більшість людей не вважає це прийнятним рішенням. Зважаючи на те, що існує спосіб C ++ 11 зробити це чисто, ніхто ніколи не повинен цього робити, якщо, можливо, вони не застрягли зі старішою версією C ++, і клас має дуже мало дзвінків до цього конструктора.
Пітер Кордес

2
@aconcernedcitizen: Я не маю на увазі причини ефективності, я маю на увазі причини якості коду. Зі свого боку, якщо ви коли-небудь змінили, як цей клас працював внутрішньо, вам доведеться перейти всі виклики до конструктора і змінити цей третій аргумент Цей додатковий ,gcd(foo, bar)код - це додатковий код, який можна і тому слід враховувати з усіх позицій джерела . Це питання ремонтопридатності / читабельності, а не продуктивності. Компілятор, швидше за все, вбудує його в час компіляції, який ви хочете виконати.
Пітер Кордес

1
@PeterCordes Ти маєш рацію, тепер я бачу, що мій погляд був зафіксований на рішенні, і я не звернув уваги на все інше. Так чи інакше, відповідь залишається, хоч лише для ганьби. Щоразу, коли у мене будуть сумніви, я буду знати, де шукати.
стурбований громадянин

1
Також розглянемо випадок, Fraction f( x+y, a+b ); щоб написати це по-своєму, вам доведеться писати BadFraction f( x+y, a+b, gcd(x+y, a+b) );або використовувати tmp vars. Або ще гірше, що, якщо ви хочете написати Fraction f( foo(x), bar(y) );- тоді вам знадобиться сайт виклику, щоб оголосити декілька tmp vars, щоб утримувати значення повернення, або викликати ці функції ще раз і сподіватися, що компілятор CSEs їх відключить, чого ми уникаємо. Ви хочете налагодити випадок одного абонента, який змішує аргументи, щоб gcdце насправді не GCD перших 2 аргументів, переданих конструктору? Ні? Тоді не робіть цю помилку можливою.
Пітер Кордес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.