Перегрузка оператора C ++ и неявное преобразование - PullRequest
7 голосов
/ 22 октября 2009

У меня есть класс, который инкапсулирует некоторую арифметику, скажем, вычисления с фиксированной точкой. Мне нравится идея перегрузки арифметических операторов, поэтому я пишу следующее:

class CFixed
{
   CFixed( int   );
   CFixed( float );
};

CFixed operator* ( const CFixed& a, const CFixed& b )
{ ... }

Все это работает. Я могу написать 3 * CFixed (0) и CFixed (3) * 10.0f. Но теперь я понимаю, что могу реализовать оператор * с целочисленным операндом гораздо более эффективным. Поэтому я перегружаю его:

CFixed operator* ( const CFixed& a, int b )
{ ... }
CFixed operator* ( int a, const CFixed& b )
{ ... }

Это все еще работает, но теперь CFixed (0) * 10.0f вызывает перегруженную версию, конвертируя float в int (и я ожидал, что она конвертирует float в CFixed). Конечно, я также могу перегружать плавающие версии, но для меня это выглядит комбинаторным взрывом кода. Есть ли обходной путь (или я неправильно проектирую свой класс)? Как я могу сказать компилятору вызывать перегруженную версию оператора * ТОЛЬКО с целыми числами?

Ответы [ 5 ]

4 голосов
/ 22 октября 2009

Вы должны также перегрузить типом float. Преобразование из int в указанный пользователем тип (CFixed) имеет более низкий приоритет, чем встроенное преобразование с плавающей интегральной переменной в float. Таким образом, компилятор всегда будет выбирать функцию с int, если только вы не добавите функцию с float.

Подробнее см. Раздел 13.3 стандарта C ++ 03. Почувствуй боль.

Кажется, я тоже это потерял. :-( UncleBens сообщает, что добавление только числа с плавающей запятой не решает проблему, поскольку следует также добавить версию с double. Но в любом случае добавление нескольких операторов, связанных со встроенными типами, утомительно, но не приводит к комбинаторному росту.

3 голосов
/ 22 октября 2009

Если у вас есть конструкторы, которые можно вызывать только с одним аргументом, вы фактически создали оператор неявного преобразования. В вашем примере, где требуется CFixed, можно передать как int, так и float. Это, конечно, опасно, потому что компилятор может молча генерировать код, вызывающий неправильную функцию, вместо того, чтобы лаять на вас, когда вы забыли включить объявление какой-либо функции.

Поэтому хорошее практическое правило гласит, что всякий раз, когда вы пишете конструкторы, которые могут быть вызваны только с одним аргументом (обратите внимание, что этот foo(int i, bool b = false) также может вызываться с одним аргументом, даже если он принимает два аргумента) , вы должны сделать этот конструктор explicit, если только вы не хотите, чтобы неявное преобразование вступило в силу. explicit конструкторы не используются компилятором для неявных преобразований.

Вы должны изменить свой класс на это:

class CFixed
{
   explicit CFixed( int   );
   explicit CFixed( float );
};

Я обнаружил, что из этого правила очень мало исключений. (std::string::string(const char*) довольно известный.)

Редактировать: Извините, я упустил момент, когда не допускал неявных преобразований из int в float.

Единственный способ предотвратить это - предоставить операторам также float.

1 голос
/ 22 октября 2009

Предполагая, что вы хотите, чтобы специализированная версия была выбрана для любого целочисленного типа (и не только для int , в частности, вы могли бы сделать это в качестве шаблонной функции и использовать Boost.EnableIf для удалите эти перегрузки из доступного набора перегрузок, если операнд не является целочисленным типом.

#include <cstdio>
#include <boost/utility/enable_if.hpp>
#include <boost/type_traits/is_integral.hpp>

class CFixed
{
public:
   CFixed( int   ) {}
   CFixed( float ) {}
};

CFixed operator* ( const CFixed& a, const CFixed&  )
{ puts("General CFixed * CFixed"); return a; }

template <class T>
typename boost::enable_if<boost::is_integral<T>, CFixed>::type operator* ( const CFixed& a, T  )
{ puts("CFixed * [integer type]"); return a; }

template <class T>
typename boost::enable_if<boost::is_integral<T>, CFixed>::type operator* ( T , const CFixed& b )
{ puts("[integer type] * CFixed"); return b; }


int main()
{
    CFixed(0) * 10.0f;
    5 * CFixed(20.4f);
    3.2f * CFixed(10);
    CFixed(1) * 100u;
}

Естественно, вы также можете использовать другое условие, чтобы сделать эти перегрузки доступными, только если T = int: typename boost::enable_if<boost::is_same<T, int>, CFixed>::type ...

Что касается разработки класса, возможно, вы могли бы больше полагаться на шаблоны. Например, конструктор может быть шаблоном, и, опять же, если вам необходимо различать целочисленный и действительный типы, то можно использовать эту технику.

0 голосов
/ 22 октября 2009

Согласитесь с sbi, вы обязательно должны сделать ваши однопараметрические конструкторы явными.

Вы можете избежать взрыва в функциях оператора <>, которые вы пишете с помощью шаблонов, однако:

template <class T>
CFixed operator* ( const CFixed& a, T b ) 
{ ... } 

template <class T>
CFixed operator* ( T a, const CFixed& b ) 
{ ... } 

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

0 голосов
/ 22 октября 2009

Как насчет сделать преобразование явным ?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...