Инициализируйте несколько постоянных членов класса, используя один вызов функции 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 в списке инициализаторов, но затем это приведет к потере памяти.

В общем, есть ли способ сделать это без напрасных вызовов функций или памяти? Можете ли вы создать временные переменные в списке инициализаторов? Спасибо.

Qq0
источник
5
У вас есть доказательства, что «функция GCD вызывается дважды»? Он упоминается дважды, но это не то же самое, что код, генерирующий компилятор, который вызывает его дважды. Компилятор может сделать вывод, что это чистая функция, и повторно использовать ее значение при втором упоминании.
Эрик Тауэрс
6
@EricTowers: Да, компиляторы иногда могут обойти эту проблему на практике в некоторых случаях. Но только если они могут видеть определение (или некоторую аннотацию в объекте), иначе нет никакого способа доказать, что это чисто. Вы должны скомпилировать с включенной оптимизацией по времени соединения, но не все это делают. И функция может быть в библиотеке. Или рассмотрим случай функции, у которой есть побочные эффекты, и вызов ее ровно один раз является вопросом правильности?
Питер Кордес
@EricTowers Интересный момент. Я действительно пытался проверить это, поместив оператор print в функцию GCD, но теперь я понимаю, что это помешает ему стать чистой функцией.
Qq0
@ Qq0: Вы можете проверить, посмотрев сгенерированный компилятором asm, например, используя проводник компилятора Godbolt с gcc или clang -O3. Но, вероятно, для любой простой реализации теста это на самом деле встроит вызов функции. Если вы используете __attribute__((const))или чистите прототип без предоставления видимого определения, он должен позволить GCC или clang выполнять исключение общих подвыражений (CSE) между двумя вызовами с одним и тем же аргументом. Обратите внимание, что ответ Дрю работает даже для не чистых функций, поэтому он намного лучше, и вы должны использовать его в любое время, когда функция может не быть встроенной.
Питер Кордес
Как правило, нестатические переменные-члены const лучше избегать. Одна из немногих областей, где часто все не применяется. Например, вы не можете назначать объекты класса. Вы можете emplace_back в вектор, но только до тех пор, пока ограничение емкости не сработает при изменении размера.
Дуги

Ответы:

66

В общем, есть ли способ сделать это без напрасных вызовов функций или памяти?

Да. Это можно сделать с помощью делегирующего конструктора , представленного в 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()вызов будет встроен в каждый сайт вызова конструктора и оставит только call3-операндный приватный конструктор.
Питер Кордес
10

Элементы-члены инициализируются в порядке их объявления в объявлении класса, поэтому вы можете сделать следующее (математически)

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

Не нужно вызывать других конструкторов или даже создавать их.

asmmo
источник
6
Хорошо, это работает специально для GCD, но многие другие варианты использования, вероятно, не могут получить 2-е const из аргументов и первого. И, как написано, у этого есть одно дополнительное деление, которое является еще одним недостатком и идеалом, который компилятор может не оптимизировать. GCD может стоить только одного деления, так что это может быть почти так же плохо, как вызов GCD дважды. (Предполагая, что разделение доминирует над стоимостью других операций, как это часто происходит на современных процессорах.)
Питер Кордес
@PeterCordes, но другое решение имеет дополнительный вызов функции и выделяет больше памяти для команд.
asmmo
1
Вы говорите о делегирующем конструкторе Дрю? Это может явно включить Fraction(a,b,gcd(a,b))делегирование в вызывающую сторону, что приведет к снижению общей стоимости. Этот компилятор легче сделать компилятору, чем отменить дополнительное деление в этом. Я не пробовал это на godbolt.org, но вы можете, если вам интересно. Используйте gcc или clang, -O3как при обычной сборке. (C ++ разработан исходя из предположения о современном оптимизирующем компиляторе, следовательно, имеет такие функции, как constexpr)
Питер Кордес
-3

@ Дрю Дорманн дал решение, подобное тому, что я имел в виду. Поскольку 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, но, по крайней мере, работает для других типов. И какого дополнительного разделения вы «также» избегаете? Вы имеете в виду против ответа Асммо?
Питер Кордес
1
Хорошо, убрал мое понижение теперь, когда вы объяснили свою точку зрения. Но это кажется довольно очевидно ужасным и требует, чтобы вы вручную вставляли часть работы конструктора в каждого вызывающего. Это противоположность СУХОЙ (не повторяйся) и инкапсуляции ответственности / внутренних дел класса. Большинство людей не считают это приемлемым решением. Принимая во внимание, что есть способ сделать это чисто на C ++ 11, никто никогда не должен этого делать, если, возможно, он не застрял в более старой версии C ++, и у класса очень мало обращений к этому конструктору.
Питер Кордес
2
@aconcernedcitizen: я имею в виду не по соображениям производительности, а по качеству кода. С вашей точки зрения, если вы когда-нибудь изменили внутреннюю работу этого класса, вам нужно было бы найти все вызовы конструктора и изменить этот 3-й аргумент. Этот дополнительный ,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 для хранения возвращаемых значений, или снова вызвать эти функции и надеяться, что компилятор CSE удалит их, чего мы и избегаем. Вы хотите отладить случай, когда один вызывающий объект смешивает аргументы, чтобы gcdна самом деле это не GCD первых двух аргументов, переданных конструктору? Нет? Тогда не делайте эту ошибку возможной.
Питер Кордес