Могу ли я получить полиморфное поведение без использования виртуальных функций? - PullRequest
17 голосов
/ 29 сентября 2010

Из-за моего устройства я не могу использовать виртуальные функции.Предположим, у меня есть:

class Base
{
    void doSomething() { }
};

class Derived : public Base
{
    void doSomething() { }
};

// in any place
{
    Base *obj = new Derived;
    obj->doSomething();
}

obj->doSomething() вызовет только Base::doSomething()

Есть ли способ с Base *obj, чтобы вызвать doSomething из Derived?

Я знаю, что могу просто поставить virtual перед doSomething() из Base, это решит проблему, но я ограничен моим устройством, компилятор его не поддерживает.

Ответы [ 11 ]

14 голосов
/ 29 сентября 2010

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

Base* obj = new Derived;
Derived* d = static_cast<Derived*>( obj ); 
d->doSomething();

Поскольку doSomething() не объявлено virtual, вы должны получить производную реализацию.

9 голосов
/ 29 сентября 2010

Конечно, вы можете сделать это;это просто не обязательно просто.

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

class Base;
class Derived1;
class Derived2;

class MemFnWrapper
{
public:

    enum DerivedType { BaseType, Derived1Type, Derived2Type };

    typedef void(Base::*BaseFnType)();
    typedef void(Derived1::*Derived1FnType)();
    typedef void(Derived2::*Derived2FnType)();

    MemFnWrapper(BaseFnType fn) : type_(BaseType) { fn_.baseFn_ = fn; }
    MemFnWrapper(Derived1FnType fn) : type_(Derived1Type) {fn_.derived1Fn_ = fn;}
    MemFnWrapper(Derived2FnType fn) : type_(Derived2Type) {fn_.derived2Fn_ = fn;}

    void operator()(Base* ptr) const;

private:

    union FnUnion
    {
        BaseFnType baseFn_;
        Derived1FnType derived1Fn_;
        Derived2FnType derived2Fn_;
    };

    DerivedType type_;
    FnUnion fn_;
};

class Base
{
public:

    Base() : doSomethingImpl(&Base::myDoSomething) { }
    Base(MemFnWrapper::Derived1FnType f) : doSomethingImpl(f) { }
    Base(MemFnWrapper::Derived2FnType f) : doSomethingImpl(f) { }

    void doSomething() { doSomethingImpl(this); }
private:
    void myDoSomething() { }
    MemFnWrapper doSomethingImpl;
};

class Derived1 : public Base
{
public:
    Derived1() : Base(&Derived1::myDoSomething) { }
private:
    void myDoSomething() { } 
};

class Derived2 : public Base
{
public:
    Derived2() : Base(&Derived2::myDoSomething) { }
private:
    void myDoSomething() { } 
};

// Complete the MemFnWrapper function call operator; this has to be after the
// definitions of Derived1 and Derived2 so the cast is valid:
void MemFnWrapper::operator()(Base* ptr) const
{
    switch (type_)
    {
    case BaseType:     return (ptr->*(fn_.baseFn_))();
    case Derived1Type: return (static_cast<Derived1*>(ptr)->*(fn_.derived1Fn_))();
    case Derived2Type: return (static_cast<Derived2*>(ptr)->*(fn_.derived2Fn_))();
    }
}

int main()
{
    Base* obj0 = new Base;
    Base* obj1 = new Derived1;
    Base* obj2 = new Derived2;
    obj0->doSomething(); // calls Base::myDoSomething()
    obj1->doSomething(); // calls Derived1::myDoSomething()
    obj2->doSomething(); // calls Derived2::myDoSomething()
}

(я изначально предлагал использовать std::function, который выполняет большую часть этой работы за вас, но потом я вспомнил, что это полиморфныйФункция-обертка, поэтому она обязательно использует виртуальные функции. :-P Упс. Вы можете просмотреть историю изменений, чтобы увидеть, как это выглядело)

6 голосов
/ 29 сентября 2010

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

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

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

Чтобы убрать шаблон с пути, вам понадобятся эти макросы.Вы можете поместить их в заголовочный файл и забыть о них, если хотите;они довольно общие.[Пожалуйста, не убегайте после прочтения этого;если вы не знакомы с библиотекой Boost.Preprocessor, она, вероятно, выглядит ужасно :-) После этого первого блока кода мы увидим, как мы можем использовать это, чтобы сделать код нашего приложения намного чище.Если вы хотите, вы можете просто проигнорировать детали этого кода.]

Код представлен в том порядке, в каком он есть, потому что если вы копируете и вставляете каждый из блоков кода из этого поста, по порядку, вИсходный файл C ++, он (я имею в виду, должен!) Компилируется и запускается.

Я назвал это «Псевдополиморфной библиотекой»;любые имена, начинающиеся с «PseudoPM» с любой заглавной буквы, должны рассматриваться как зарезервированные им.Макросы, начинающиеся с PSEUDOPM, являются публично вызываемыми макросами;макросы, начинающиеся с PSEUDOPMX, предназначены для внутреннего использования.

#include <boost/preprocessor.hpp>

// [INTERNAL] PSEUDOPM_INIT_VTABLE Support
#define PSEUDOPMX_INIT_VTABLE_ENTRY(r, c, i, fn)                              \
  BOOST_PP_COMMA_IF(BOOST_PP_NOT_EQUAL(0, i))                                 \
  & c :: BOOST_PP_CAT(BOOST_PP_TUPLE_ELEM(4, 0, fn), Impl)

// [INTERNAL] PSEUDOPM_DECLARE_VTABLE Support
#define PSEUDOPMX_DECLARE_VTABLE_STRUCT_MEMBER(r, c, i, fn)                   \
  BOOST_PP_TUPLE_ELEM(4, 1, fn)                                               \
  (c :: * BOOST_PP_CAT(BOOST_PP_TUPLE_ELEM(4, 0, fn), Ptr))                   \
  BOOST_PP_TUPLE_ELEM(4, 3, fn);

#define PSEUDOPMX_DECLARE_VTABLE_STRUCT(r, memfns, c)                         \
  struct BOOST_PP_CAT(PseudoPMIntVTable, c)                                   \
  {                                                                           \
    friend class c;                                                           \
    BOOST_PP_SEQ_FOR_EACH_I(PSEUDOPMX_DECLARE_VTABLE_STRUCT_MEMBER, c, memfns)\
  };

#define PSEUDOPMX_DECLARE_VTABLE_ENUM_MEMBER(r, x, i, c)                      \
  BOOST_PP_COMMA_IF(BOOST_PP_NOT_EQUAL(0, i)) BOOST_PP_CAT(PseudoPMType, c)

#define PSEUDOPMX_DECLARE_VTABLE_UNION_MEMBER(r, x, c)                        \
  BOOST_PP_CAT(PseudoPMIntVTable, c) BOOST_PP_CAT(BOOST_PP_CAT(table_, c), _);

#define PSEUDOPMX_DECLARE_VTABLE_RESET_FN(r, x, c)                            \
  void Reset(BOOST_PP_CAT(PseudoPMIntVTable, c) table)                        \
  {                                                                           \
    type_ = BOOST_PP_CAT(PseudoPMType, c);                                    \
    table_.BOOST_PP_CAT(BOOST_PP_CAT(table_, c), _) = table;                  \
  }

#define PSEUDOPMX_DECLARE_VTABLE_PUBLIC_FN(r, x, fn)                          \
  BOOST_PP_TUPLE_ELEM(4, 1, fn)                                               \
  BOOST_PP_TUPLE_ELEM(4, 0, fn)                                               \
  BOOST_PP_TUPLE_ELEM(4, 3, fn);

// [INTERNAL] PSEUDOPM_DEFINE_VTABLE Support
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST0
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST1 a0
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST2 a0, a1
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST3 a0, a1, a2
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST4 a0, a1, a2, a3
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST5 a0, a1, a2, a3, a4
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST6 a0, a1, a2, a3, a4, a5
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST7 a0, a1, a2, a3, a4, a5, a6
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST8 a0, a1, a2, a3, a4, a5, a6, a7
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST9 a0, a1, a2, a3, a4, a5, a6, a7, a8

#define PSEUDOPMX_DEFINE_VTABLE_FNP(r, x, i, t)                               \
  BOOST_PP_COMMA_IF(BOOST_PP_NOT_EQUAL(0, i))                                 \
  t BOOST_PP_CAT(a, i)

#define PSEUDOPMX_DEFINE_VTABLE_FN_CASE(r, fn, i, c)                          \
  case BOOST_PP_CAT(PseudoPMType, c) : return                                 \
  (                                                                           \
    static_cast<c*>(this)->*pseudopm_vtable_.table_.                          \
    BOOST_PP_CAT(BOOST_PP_CAT(table_, c), _).                                 \
    BOOST_PP_CAT(BOOST_PP_TUPLE_ELEM(4, 0, fn), Ptr)                          \
  )(                                                                          \
    BOOST_PP_CAT(                                                             \
      PSEUDOPMX_DEFINE_VTABLE_ARGLIST,                                        \
      BOOST_PP_TUPLE_ELEM(4, 2, fn)                                           \
    )                                                                         \
  );

#define PSEUDOPMX_DEFINE_VTABLE_FN(r, classes, fn)                            \
  BOOST_PP_TUPLE_ELEM(4, 1, fn)                                               \
  BOOST_PP_SEQ_HEAD(classes) :: BOOST_PP_TUPLE_ELEM(4, 0, fn)                 \
  (                                                                           \
    BOOST_PP_SEQ_FOR_EACH_I(                                                  \
      PSEUDOPMX_DEFINE_VTABLE_FNP, x,                                         \
      BOOST_PP_TUPLE_TO_SEQ(                                                  \
        BOOST_PP_TUPLE_ELEM(4, 2, fn),                                        \
        BOOST_PP_TUPLE_ELEM(4, 3, fn)                                         \
      )                                                                       \
    )                                                                         \
  )                                                                           \
  {                                                                           \
    switch (pseudopm_vtable_.type_)                                           \
    {                                                                         \
      BOOST_PP_SEQ_FOR_EACH_I(PSEUDOPMX_DEFINE_VTABLE_FN_CASE, fn, classes)   \
    }                                                                         \
  }

// Each class in the classes sequence should call this macro at the very 
// beginning of its constructor.  'c' is the name of the class for which
// to initialize the vtable, and 'memfns' is the member function sequence.
#define PSEUDOPM_INIT_VTABLE(c, memfns)                                       \
  BOOST_PP_CAT(PseudoPMIntVTable, c) pseudopm_table =                         \
  {                                                                           \
    BOOST_PP_SEQ_FOR_EACH_I(PSEUDOPMX_INIT_VTABLE_ENTRY, c, memfns)           \
  };                                                                          \
  pseudopm_vtable_.Reset(pseudopm_table); 

// The base class should call this macro in its definition (at class scope).
// This defines the virtual table structs, enumerations, internal functions, 
// and declares the public member functions.  'classes' is the sequence of
// classes and 'memfns' is the member function sequence.
#define PSEUDOPM_DECLARE_VTABLE(classes, memfns)                              \
  protected:                                                                  \
  BOOST_PP_SEQ_FOR_EACH(PSEUDOPMX_DECLARE_VTABLE_STRUCT, memfns, classes)     \
                                                                              \
  enum PseudoPMTypeEnum                                                       \
  {                                                                           \
    BOOST_PP_SEQ_FOR_EACH_I(PSEUDOPMX_DECLARE_VTABLE_ENUM_MEMBER, x, classes) \
  };                                                                          \
                                                                              \
  union PseudoPMVTableUnion                                                   \
  {                                                                           \
    BOOST_PP_SEQ_FOR_EACH(PSEUDOPMX_DECLARE_VTABLE_UNION_MEMBER, x, classes)  \
  };                                                                          \
                                                                              \
  class PseudoPMVTable                                                        \
  {                                                                           \
  public:                                                                     \
    BOOST_PP_SEQ_FOR_EACH(PSEUDOPMX_DECLARE_VTABLE_RESET_FN, x, classes)      \
  private:                                                                    \
    friend class BOOST_PP_SEQ_HEAD(classes);                                  \
    PseudoPMTypeEnum type_;                                                   \
    PseudoPMVTableUnion table_;                                               \
  };                                                                          \
                                                                              \
  PseudoPMVTable pseudopm_vtable_;                                            \
                                                                              \
  public:                                                                     \
  BOOST_PP_SEQ_FOR_EACH(PSEUDOPMX_DECLARE_VTABLE_PUBLIC_FN, x, memfns)

// This macro must be called in some source file after all of the classes in
// the classes sequence have been defined (so, for example, you can create a 
// .cpp file, include all the class headers, and then call this macro.  It 
// actually defines the public member functions for the base class.  Each of 
// the public member functions calls the correct member function in the 
// derived class.  'classes' is the sequence of classes and 'memfns' is the 
// member function sequence.
#define PSEUDOPM_DEFINE_VTABLE(classes, memfns)                               \
  BOOST_PP_SEQ_FOR_EACH(PSEUDOPMX_DEFINE_VTABLE_FN, classes, memfns)

(Мы должны сделать vtable статическим, но я оставлю это в качестве упражнения для читателя. :-D)

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

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

// The sequence of classes in the class hierarchy.  The base class must be the
// first class in the sequence.  Derived classes can be in any order.
#define CLASSES (Base)(Derived)

Во-вторых, нам нужно определить список «виртуальных» функций-членов.Обратите внимание, что в этой (предположительно ограниченной) реализации базовый класс и каждый производный класс должны реализовывать каждую из «виртуальных» функций-членов.Если класс не определит один из них, компилятор рассердится.

// The sequence of "virtual" member functions.  Each entry in the sequence is a
// four-element tuple:
// (1) The name of the function.  A function will be declared in the Base class
//     with this name; it will do the dispatch.  All of the classes in the class
//     sequence must implement a private implementation function with the same 
//     name, but with "Impl" appended to it (so, if you declare a function here 
//     named "Foo" then each class must define a "FooImpl" function.
// (2) The return type of the function.
// (3) The number of arguments the function takes (arity).
// (4) The arguments tuple.  Its arity must match the number specified in (3).
#define VIRTUAL_FUNCTIONS               \
  ((FuncNoArg,  void, 0, ()))           \
  ((FuncOneArg, int,  1, (int)))        \
  ((FuncTwoArg, int,  2, (int, int)))

Обратите внимание, что вы можете назвать эти два макроса как угодно;вам просто нужно обновить ссылки в следующих фрагментах.

Далее мы можем определить наши классы.В базовом классе нам нужно вызвать PSEUDOPM_DECLARE_VTABLE, чтобы объявить виртуальные функции-члены и определить весь шаблон для нас.Во всех наших конструкторах классов нам нужно вызывать PSEUDOPM_INIT_VTABLE;этот макрос генерирует код, необходимый для правильной инициализации vtable.

В каждом классе мы также должны определить все функции-члены, перечисленные выше в последовательности VIRTUAL_FUNCTIONS.Обратите внимание, что мы должны назвать реализации с суффиксом Impl;это потому, что реализации всегда вызываются через функции диспетчера, которые генерируются макросом PSEUDOPM_DECLARE_VTABLE.

class Base 
{ 
public: 
    Base()
    {
      PSEUDOPM_INIT_VTABLE(Base, VIRTUAL_FUNCTIONS)
    }

    PSEUDOPM_DECLARE_VTABLE(CLASSES, VIRTUAL_FUNCTIONS)
private:
    void FuncNoArgImpl() { }
    int FuncOneArgImpl(int x) { return x; }
    int FuncTwoArgImpl(int x, int y) { return x + y; }
}; 

class Derived : public Base 
{
public: 
    Derived() 
    { 
        PSEUDOPM_INIT_VTABLE(Derived, VIRTUAL_FUNCTIONS)
    } 
private: 
    void FuncNoArgImpl() { }
    int FuncOneArgImpl(int x) { return 2 * x; }
    int FuncTwoArgImpl(int x, int y) { return 2 * (x + y); }
};

Наконец, в некоторый исходный файл вам нужно будет включить все заголовки, где всеклассы определены и вызывают макрос PSEUDOPM_DEFINE_VTABLE;этот макрос фактически определяет функции диспетчера.Этот макрос не может использоваться, если все классы еще не определены (он должен static_cast указатель this базового класса *, и это не удастся, если компилятор не знает, что производный класс на самом деле является производным отбазовый класс).

PSEUDOPM_DEFINE_VTABLE(CLASSES, VIRTUAL_FUNCTIONS)

Вот некоторый тестовый код, демонстрирующий функциональность:

#include <cassert>

int main() 
{ 
    Base* obj0 = new Base; 
    Base* obj1 = new Derived; 
    obj0->FuncNoArg(); // calls Base::FuncNoArg
    obj1->FuncNoArg(); // calls Derived::FuncNoArg

    assert(obj0->FuncTwoArg(2, 10) == 12); // Calls Base::FuncTwoArg
    assert(obj1->FuncTwoArg(2, 10) == 24); // Calls Derived::FuncTwoArg
} 

[Отказ от ответственности: Этот код протестирован только частично.Может содержать ошибки.(На самом деле, вероятно, так и есть; большую часть времени я написал в 1 час утра :-P)]

6 голосов
/ 29 сентября 2010

Вы можете понижать объект до типа Derived и вызывать его, например, так:

static_cast<Derived*>(obj)->doSomething();

, хотя это не дает никаких гарантий того, что объект 'obj' действительно имеет тип Derived.

Меня больше беспокоит, что у вас даже нет доступа к виртуальным функциям.Как работают деструкторы, если ни одна из ваших функций не может быть виртуальной, а вы создаете подклассы?

2 голосов
/ 29 сентября 2010

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

Приведение указателя, как предложено ceretullis, вероятно, первое, что вы должны рассмотреть. Но решение, которое я публикую здесь, по крайней мере, дает вам возможность написать код, который использует эти классы, как если бы ваш компилятор поддерживал virtual. То есть с простым вызовом функции.

Это программа, которая реализует класс Base с функцией, которая возвращает string: "base", и класс Derived, который возвращает string: "der". Идея состоит в том, чтобы иметь возможность поддерживать такой код:

Base* obj = new Der;
cout << obj->get_string();

... так что вызов get_string() вернет "der", даже если мы вызываем указатель Base и используем компилятор, который не поддерживает virtual.

Он работает путем реализации нашей собственной версии vtable. На самом деле, это не совсем стол. Это просто указатель на функцию-член в базовом классе. В реализации базового класса get_string(), если указатель на функцию-член не равен нулю, вызывается функция. Если он равен null, реализация базового класса выполняется.

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

#include <cstdlib>
#include <string>
#include <iostream>
using namespace std;

class Base
{
public:
    typedef string (Base::*vptr_get_string)(void) const;
    Base(vptr_get_string=0);
    void set_derived_pointer(Base* derived);

    string get_string() const;

protected:
    Base* der_ptr_;
    vptr_get_string get_string_vf_;
};

Base::Base(vptr_get_string get_string_vf)
:   der_ptr_(0),
    get_string_vf_(get_string_vf)
{
}

void Base::set_derived_pointer(Base* derived)
{
    der_ptr_ = derived;
}

string Base::get_string() const
{
    if( get_string_vf_ )
        return (der_ptr_->*get_string_vf_)();
    else
        return "base";
}

class Der : public Base
{
public:
    Der();
    string get_string() const;
};

Der::Der()
:   Base(static_cast<Base::vptr_get_string>(&Der::get_string))
{
    set_derived_pointer(this);
}

string Der::get_string() const
{
    return "der";
}

int main()
{
    Base* obj = new Der;
    cout << obj->get_string();
    delete obj;
}
2 голосов
/ 29 сентября 2010

Вы можете сделать свой собственный vtable, я полагаю. Я просто был бы структурой, содержащей ваши «виртуальные» указатели на функции как часть Base, и имел бы код для настройки vtable.

Это довольно грубое решение - работа компилятора C ++ для обработки этой функции.

Но здесь идет:

#include <iostream>

class Base
{
protected:
    struct vt {
        void (*vDoSomething)(void);
    } vt;
private:
    void doSomethingImpl(void) { std::cout << "Base doSomething" << std::endl; }
public:
    void doSomething(void) { (vt.vDoSomething)();}
    Base() : vt() { vt.vDoSomething = (void(*)(void)) &Base::doSomethingImpl;}
};

class Derived : public Base
{
public:
    void doSomething(void) { std::cout << "Derived doSomething" << std::endl; }
    Derived() : Base() { vt.vDoSomething = (void(*)(void)) &Derived::doSomething;}
};
1 голос
/ 29 сентября 2010

Можете ли вы инкапсулировать базовый класс, а не наследовать от него?

Затем вы можете вызвать doSomething () // получить производное
или base-> doSomething () // вызывает base

0 голосов
/ 10 мая 2019

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

#include<iostream>

using namespace std;

 class Base
{
  public:

   void display()
    {
      cout<<"From Base class\n";
    }
 };

 class Derived:public Base
 {
   public:

    void display()
    {
      cout<<"From Derived class";

    }
   };

int main()
{
  Base *ptr=new Derived;
  Derived* d = static_cast<Derived*>(ptr);
  ptr->display();
  d->display();
  return 0;
}

Выход:

Из базового класса Из производного класса

0 голосов
/ 30 сентября 2010

Я думаю, что это возможно с CRTP (если ваше «Устройство» поддерживает шаблоны).

#include <iostream>

template<class T> struct base{
    void g(){
        if(T *p = static_cast<T *>(this)){
            p->f();
        }
    }
    void f(){volatile int v = 0; std::cout << 1;}
    virtual ~base(){}
};

struct derived1 : base<derived1>{
    void f(){std::cout << 2;}
};

struct derived2 : base<derived2>{
    void f(){std::cout << 3;}
};

int main(){
    derived1 d1;
    d1.g();

    derived2 d2;
    d2.g();
}
0 голосов
/ 30 сентября 2010

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

template<class SomethingDoer> class MyClass
{
    public:
        void doSomething() {myDoer.doSomething();}
    private:
        SomethingDoer myDoer;
};

class BaseSomethingDoer
{
    public:
        void doSomething() { // base implementation }
};

class DerivedSomethingDoer
{
    public:
        void doSomething() { // derived implementation }
};

typedef MyClass<BaseSomethingDoer> Base;
typedef MyClass<DerivedSomethingDoer> Derived;

Теперь мы не можем указывать на Derived с указателем Base, но у нас могут быть шаблонные функции, которые принимают MyClass и будут работать как с объектами Base, так и Derived.

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