лучший дизайн, чем круговая зависимость для конвертируемых типов - PullRequest
1 голос
/ 06 января 2012

Я сталкивался с кодом, в котором есть два класса, которые должны быть конвертируемыми между собой, и проект должен был обеспечить конструктор с одним аргументом для каждого класса, принимающего другой класс, например, в файле A.hpp

class B;
class A
{
  explicit A( const B& );
  ...
};

и B.hpp

class A;
class B
{
  explicit B( const A& );
  ...
};

, которые вводят круговую зависимость между двумя классами (чего я бы предпочел избежать, но, возможно, это не имеет большого значения).Есть ли лучший дизайн? другой дизайн будет иметь другой класс преобразователя, который мог бы выполнять эту работу, например, в C.hpp

#include "A.hpp"
#include "B.hpp"
class C
{
  static A toA( const B& );
  static B toB( const A& );
  ...  
};

или в функциях области имен, например, в D.hpp

#include "A.hpp"
#include "B.hpp"
namespace D
{
  A toA( const B& );
  B toB( const A& );
  ...
}

но я не уверен, что один из них лучше .Есть ли явно лучшая альтернатива любому из этих решений?

Ответы [ 5 ]

2 голосов
/ 06 января 2012

Я думаю, что можно оставить один из двух типов в покое и использовать другой класс для преобразования в и из этого другого типа:

class A {};

class B {
public:
    B(A a) { ... }
    operator A() const { ... }
};

Таким образом, B может быть построен из A, а B может быть преобразован в A.

1 голос
/ 06 января 2012

Пока оба конструктора explicit, я не вижу проблемы. Это не неявные преобразования, поэтому они довольно безопасны.

Кстати, этот тип циклической зависимости является обобщением рекурсивных определений, таких как это:

struct List {
  List(List * next) : _next(next) {}
  List * _next;
};

Здесь List определяется в терминах List и состоит из List. Ваши занятия являются примером взаимной рекурсии. В этом нет ничего плохого.

1 голос
/ 06 января 2012

Из комментариев

Обычно я стараюсь избегать их, но в своем вопросе я упомянул, что в этом случае они могут не иметь большого значения; однако, что если бы у меня был целый алфавит классов: тогда, вероятно, полезный класс или пространство имен было бы лучше, чем если бы A-Z зависели друг от друга, нет?

Если у вас большое количество взаимопревращаемых классов, вы должны предоставить общий интерфейс между ними (или попытаться предоставить один для большого подмножества). Затем определите шаблонный конструктор,

class A
{
  template< typename other > // one template instead of many functions
  explicit A( const other &arg, // user just passes by reference as usual
      typename enable_if< // SFINAE restricts template to proper cases
      other::implements_common_interface, // trait signals such cases
      int >::type = 0 ) { // only generate a dummy parameter
      ; // define implementation in header, as with any template
  } // dependency is resolved when template is used, i.e. called

  enum { implements_common_interface = true }; // define trait
};

Теперь все классы, имеющие общий интерфейс и определяющие implements_common_interface = true, могут быть преобразованы друг в друга без явной зависимости.

1 голос
/ 06 января 2012

Циркулярная зависимость сама по себе не является плохой вещью, если она имеет смысл в реальном сценарии, который вы моделируете.Например, если два класса, BigDecimal и BigInteger, оба представляют числа (предположительно, используя различное представление), имеет смысл позволить одному быть построенным из другого, и позволить ему идти обоими путями.

Добавление третьего класса может быть лучшим вариантом, но только в том случае, если его использование не вынуждает вас на территорию "дружбы" или вынуждает вас писать существенно худший код, чтобы избежать объявлений "дружбы".Синтаксис тоже стал бы менее «свободным».Иногда это хорошо, например, когда вы конвертируете строки в числа.Но бывают случаи, когда лишние convert_to раздражают, например, когда вы строите строки C ++ из строк C (но не наоборот).

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

0 голосов
/ 06 января 2012

Вот одна альтернатива, которая позволяет полиморфно работать с обоими объектами:

void convert_to_b(const A &a, B &b);
void convert_to_a(const B &b, A &a);

Это будет просто передавать данные между A и B, и он может вызывать функции-члены обоих для выполнения своей работы.

...