Может ли шаблон функции члена класса быть виртуальным?


304

Я слышал, что шаблоны функций-членов класса C ++ не могут быть виртуальными. Это правда?

Если они могут быть виртуальными, то каков пример сценария, в котором можно использовать такую ​​функцию?


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

Ответы:


329

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

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

Тем не менее, есть несколько мощных и интересных методов, вытекающих из сочетания полиморфизма и шаблонов, в частности, так называемое стирание типов .


32
Я не вижу языковой причины для этого, только причины реализации . vtables не являются частью языка - просто стандартный способ, которым компиляторы реализуют язык.
Gerardw

16
Virtual functions are all about the run-time system figuring out which function to call at run-time- извините, но это довольно неправильный путь и довольно запутанный. Это просто косвенное обращение, и в нем не задействовано «выяснение времени выполнения», во время компиляции известно, что вызываемая функция - это та, на которую указывает n-й указатель в vtable. «Выяснение» подразумевает, что есть проверки типа и тому подобное, что не так. Once the run-time system figured out it would need to call a templatized virtual function- является ли функция виртуальной, известно во время компиляции.
Dtech

9
@ddriver: 1. Если компилятор видит void f(concr_base& cb, virt_base& vb) { cb.f(); vb.f(); }, то он «знает», какая функция вызывается в вызываемой точке cb.f(), и не знает, для чего vb.f(). Последнее должно быть обнаружено во время выполнения , исполняющей системой . Если вы хотите назвать это «выяснением», и является ли это более или менее эффективным, это немного не меняет эти факты.
суббота,

9
@ddriver: 2. Экземпляры шаблонов функций (членов) являются функциями (членами), поэтому проблем с размещением указателя на такой экземпляр в виртуальной таблице не возникает. Но какие экземпляры шаблонов необходимы, известно только при компиляции вызывающей стороны, тогда как vtables устанавливаются при компиляции базового класса и производных классов. И все это составлено отдельно. Еще хуже - новые производные классы могут быть связаны с работающими системами во время выполнения (подумайте, что ваш браузер загружает плагин динамически). Даже исходный код вызывающей стороны может быть долго потерян при создании нового производного класса.
суббота,

9
@sbi: Почему вы делаете предположения на основе моего имени? Я не перепутал дженерики и шаблоны. Я знаю, что дженерики Java - просто время выполнения. Вы не исчерпывающе объяснили, почему у вас не может быть шаблонов виртуальных функций-членов в C ++, но InQsitive сделал. Вы слишком упростили шаблон и виртуальную механику для «времени компиляции» против «времени выполнения» и пришли к выводу, что «у вас не может быть шаблонов виртуальных функций-членов». Я сослался на ответ InQsitive, который ссылается на «Шаблоны C ++ Полное руководство». Я не считаю это "маханием рукой". Хорошего дня.
Javanator

133

Из шаблонов C ++ Полное руководство:

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


8
Я думаю, что сегодняшний компилятор C ++ и компоновщики, особенно с поддержкой оптимизации времени компоновки, должны иметь возможность генерировать необходимые таблицы и смещения во время компоновки. Так, может быть, мы получим эту функцию в C ++ 2b?
Кай Петцке

33

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

Когда вам понадобится шаблонная функция-член? Однажды я столкнулся с такой ситуацией, когда я пытался реорганизовать иерархию с чисто виртуальным базовым классом. Это был плохой стиль для реализации различных стратегий. Я хотел изменить аргумент одной из виртуальных функций на числовой тип и вместо перегрузки функции-члена и переопределить каждую перегрузку во всех подклассах, я пытался использовать функции виртуального шаблона (и должен был выяснить, их не существует). .)


5
@pmr: виртуальная функция может быть вызвана из кода, который даже не существовал, когда функция была скомпилирована. Как компилятор определит, какие экземпляры (теоретической) виртуальной функции-члена шаблона генерировать для кода, который даже не существует?
2010 года

2
@sbi: Да, отдельная компиляция была бы огромной проблемой. Я вообще не специалист по компиляторам C ++, поэтому не могу предложить решение. Как и в случае с шаблонными функциями в целом, его следует снова создавать в каждой единице компиляции, верно? Разве это не решит проблему?
ПМР

2
@sbi, если вы имеете в виду динамическую загрузку библиотек, это общая проблема с шаблонными классами / функциями, а не только с виртуальными шаблонными методами.
Дуб

«C ++ не позволяет [...]» - был бы признателен за ссылку на стандарт (независимо от того, какой из них был актуален, когда был написан ответ, или тот, который был обновлен через восемь лет) ...
Аконкагуа

19

Таблицы виртуальных функций

Давайте начнем с некоторой предыстории виртуальных таблиц функций и того, как они работают ( источник ):

[20.3] В чем разница между вызовом виртуальных и не виртуальных функций-членов?

Не виртуальные функции-члены разрешаются статически. То есть функция-член выбирается статически (во время компиляции) в зависимости от типа указателя (или ссылки) на объект.

Напротив, виртуальные функции-члены разрешаются динамически (во время выполнения). То есть функция-член выбирается динамически (во время выполнения) в зависимости от типа объекта, а не от типа указателя / ссылки на этот объект. Это называется «динамическое связывание». Большинство компиляторов используют какой-либо вариант следующего метода: если объект имеет одну или несколько виртуальных функций, компилятор помещает в объект скрытый указатель, называемый «виртуальный указатель» или «v-указатель». Этот v-указатель указывает на глобальную таблицу, называемую «виртуальная таблица» или «v-таблица».

Компилятор создает v-таблицу для каждого класса, который имеет хотя бы одну виртуальную функцию. Например, если у класса Circle есть виртуальные функции для draw () и move () и resize (), то будет точно одна v-таблица, связанная с классом Circle, даже если бы существовал gazillion объектов Circle, и указатель v каждый из этих объектов Circle будет указывать на v-таблицу Circle. В самой v-таблице есть указатели на каждую из виртуальных функций в классе. Например, v-таблица Circle будет иметь три указателя: указатель на Circle :: draw (), указатель на Circle :: move () и указатель на Circle :: resize ().

Во время отправки виртуальной функции система времени выполнения следует v-указателю объекта на v-таблицу класса, затем следует за соответствующим слотом в v-таблице к коду метода.

Затраты на пространство для вышеуказанного метода являются номинальными: дополнительный указатель на объект (но только для объектов, которым потребуется динамическое связывание), плюс дополнительный указатель на метод (но только для виртуальных методов). Затраты времени и средств также довольно номинальны: по сравнению с обычным вызовом функции, вызов виртуальной функции требует двух дополнительных выборок (одна для получения значения v-указателя, вторая для получения адреса метода). Ни одно из этих действий во время выполнения не происходит с не виртуальными функциями, поскольку компилятор разрешает не виртуальные функции исключительно во время компиляции на основе типа указателя.


Моя проблема, или как я сюда попал

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

Некоторый код:

virtual void  LoadCube(UtpBipCube<float> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void  LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void  LoadCube(UtpBipCube<unsigned short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;

То, что я хотел бы, но это не скомпилируется из-за виртуальной шаблонной комбинации:

template<class T>
    virtual void  LoadCube(UtpBipCube<T> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
            long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;

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

Решение

предупреждение, это не очень красиво, но это позволило мне удалить повторяющийся код выполнения

1) в базовом классе

virtual void  LoadCube(UtpBipCube<float> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
            long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void  LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
            long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void  LoadCube(UtpBipCube<unsigned short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
            long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;

2) и в детских классах

void  LoadCube(UtpBipCube<float> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1)
{ LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); }

void  LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1)
{ LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); }

void  LoadCube(UtpBipCube<unsigned short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1)
{ LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); }

template<class T>
void  LoadAnyCube(UtpBipCube<T> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1);

Обратите внимание, что LoadAnyCube не объявлен в базовом классе.


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


1
Я встречал такую ​​же ситуацию и со структурой наследования массовых классов. Макросы помогли.
ZFY

16

Следующий код можно скомпилировать и запустить правильно, используя MinGW G ++ 3.4.5 в Windows 7:

#include <iostream>
#include <string>

using namespace std;

template <typename T>
class A{
public:
    virtual void func1(const T& p)
    {
        cout<<"A:"<<p<<endl;
    }
};

template <typename T>
class B
: public A<T>
{
public:
    virtual void func1(const T& p)
    {
        cout<<"A<--B:"<<p<<endl;
    }
};

int main(int argc, char** argv)
{
    A<string> a;
    B<int> b;
    B<string> c;

    A<string>* p = &a;
    p->func1("A<string> a");
    p = dynamic_cast<A<string>*>(&c);
    p->func1("B<string> c");
    B<int>* q = &b;
    q->func1(3);
}

и вывод:

A:A<string> a
A<--B:B<string> c
A<--B:3

И позже я добавил новый класс X:

class X
{
public:
    template <typename T>
    virtual void func2(const T& p)
    {
        cout<<"C:"<<p<<endl;
    }
};

Когда я попытался использовать класс X в main () следующим образом:

X x;
x.func2<string>("X x");

g ++ сообщает о следующей ошибке:

vtempl.cpp:34: error: invalid use of `virtual' in template declaration of `virtu
al void X::func2(const T&)'

Итак, очевидно, что:

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

19
Шаблон класса может иметь виртуальные функции-члены. Функция-член не может быть одновременно шаблоном функции-члена и виртуальной функцией-членом.
Джеймс МакНеллис

1
это фактически терпит неудачу с gcc 4.4.3. На моей системе наверняка Ubuntu 10.04
blueskin

3
Это полностью отличается от того, что задан вопрос. Здесь весь базовый класс является шаблонным. Я собирал подобные вещи раньше. Это скомпилируется и в Visual Studio 2010
ds-bos-msk

14

Нет, они не могут. Но:

template<typename T>
class Foo {
public:
  template<typename P>
  void f(const P& p) {
    ((T*)this)->f<P>(p);
  }
};

class Bar : public Foo<Bar> {
public:
  template<typename P>
  void f(const P& p) {
    std::cout << p << std::endl;
  }
};

int main() {
  Bar bar;

  Bar *pbar = &bar;
  pbar -> f(1);

  Foo<Bar> *pfoo = &bar;
  pfoo -> f(1);
};

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


3
Это известно как CRTP, если кому-то интересно.
Майкл Чой

1
Но это не помогает в тех случаях, когда человек имеет иерархию классов и хочет иметь возможность вызывать виртуальные методы указателей на базовые классы. Ваш Fooуказатель квалифицирован как Foo<Bar>, он не может указывать на Foo<Barf>или Foo<XXX>.
Кай Петцке

@KaiPetzke: Вы не можете создать неограниченный указатель, нет. Но вы можете шаблонизировать любой код, которому не нужно знать конкретный тип, который имеет почти такой же эффект (по крайней мере, концептуально - очевидно, совершенно другую реализацию).
Том

8

Нет, функции-члены шаблона не могут быть виртуальными.


9
Мое любопытство: почему? С какими проблемами сталкивается компилятор?
WannaBeGeek

1
Вам нужно объявление в области видимости (по крайней мере, чтобы получить правильные типы). В соответствии со стандартом (и языком) требуется наличие в области видимости для идентификаторов, которые вы используете.
Dirkgently

4

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

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

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

Однако для каждой комбинации типов шаблонов необходимо определить фиктивную функцию виртуальной оболочки:

#include <memory>
#include <iostream>
#include <iomanip>

//---------------------------------------------
// Abstract class with virtual functions
class Geometry {
public:
    virtual void getArea(float &area) = 0;
    virtual void getArea(long double &area) = 0;
};

//---------------------------------------------
// Square
class Square : public Geometry {
public:
    float size {1};

    // virtual wrapper functions call template function for square
    virtual void getArea(float &area) { getAreaT(area); }
    virtual void getArea(long double &area) { getAreaT(area); }

private:
    // Template function for squares
    template <typename T>
    void getAreaT(T &area) {
        area = static_cast<T>(size * size);
    }
};

//---------------------------------------------
// Circle
class Circle : public Geometry  {
public:
    float radius {1};

    // virtual wrapper functions call template function for circle
    virtual void getArea(float &area) { getAreaT(area); }
    virtual void getArea(long double &area) { getAreaT(area); }

private:
    // Template function for Circles
    template <typename T>
    void getAreaT(T &area) {
        area = static_cast<T>(radius * radius * 3.1415926535897932385L);
    }
};


//---------------------------------------------
// Main
int main()
{
    // get area of square using template based function T=float
    std::unique_ptr<Geometry> geometry = std::make_unique<Square>();
    float areaSquare;
    geometry->getArea(areaSquare);

    // get area of circle using template based function T=long double
    geometry = std::make_unique<Circle>();
    long double areaCircle;
    geometry->getArea(areaCircle);

    std::cout << std::setprecision(20) << "Square area is " << areaSquare << ", Circle area is " << areaCircle << std::endl;
    return 0;
}

Вывод:

Площадь квадрата 1, площадь круга 3.1415926535897932385

Попробуй здесь


3

Чтобы ответить на вторую часть вопроса:

Если они могут быть виртуальными, то каков пример сценария, в котором можно использовать такую ​​функцию?

Это не необоснованная вещь, чтобы хотеть сделать. Например, Java (где каждый метод является виртуальным) не имеет проблем с общими методами.

Одним из примеров использования C ++ шаблона виртуальной функции в C ++ является функция-член, которая принимает универсальный итератор. Или функция-член, которая принимает объект универсальной функции.

Решением этой проблемы является использование стирания типов с boost :: any_range и boost :: function, что позволит вам принять универсальный итератор или функтор без необходимости превращать вашу функцию в шаблон.


6
Обобщения Java являются синтаксическим сахаром для кастинга. Они не совпадают с шаблонами.
Брайс М. Демпси

2
@ BriceM.Dempsey: Вы могли бы сказать, что приведение - это способ, которым Java реализует обобщения, а не наоборот ... и, с точки зрения семантики, представленные исключения из сценария использования являются действительными IMO.
einpoklum

2

Существует обходной путь для «метода виртуального шаблона», если набор типов для метода шаблона известен заранее.

Чтобы показать идею, в приведенном ниже примере используются только два типа ( intи double).

Там «виртуальный» метод шаблона ( Base::Method) вызывает соответствующий виртуальный метод (один из Base::VMethod), который, в свою очередь, вызывает реализацию метода шаблона (Impl::TMethod ).

Нужно только реализовать метод шаблона TMethodв производных реализациях ( AImpl, BImpl) и использовать Derived<*Impl>.

class Base
{
public:
    virtual ~Base()
    {
    }

    template <typename T>
    T Method(T t)
    {
        return VMethod(t);
    }

private:
    virtual int VMethod(int t) = 0;
    virtual double VMethod(double t) = 0;
};

template <class Impl>
class Derived : public Impl
{
public:
    template <class... TArgs>
    Derived(TArgs&&... args)
        : Impl(std::forward<TArgs>(args)...)
    {
    }

private:
    int VMethod(int t) final
    {
        return Impl::TMethod(t);
    }

    double VMethod(double t) final
    {
        return Impl::TMethod(t);
    }
};

class AImpl : public Base
{
protected:
    AImpl(int p)
        : i(p)
    {
    }

    template <typename T>
    T TMethod(T t)
    {
        return t - i;
    }

private:
    int i;
};

using A = Derived<AImpl>;

class BImpl : public Base
{
protected:
    BImpl(int p)
        : i(p)
    {
    }

    template <typename T>
    T TMethod(T t)
    {
        return t + i;
    }

private:
    int i;
};

using B = Derived<BImpl>;

int main(int argc, const char* argv[])
{
    A a(1);
    B b(1);
    Base* base = nullptr;

    base = &a;
    std::cout << base->Method(1) << std::endl;
    std::cout << base->Method(2.0) << std::endl;

    base = &b;
    std::cout << base->Method(1) << std::endl;
    std::cout << base->Method(2.0) << std::endl;
}

Вывод:

0
1
2
3

NB: Base::Methodна самом деле излишки для реального кода ( VMethodмогут быть обнародованы и использованы напрямую). Я добавил его так, чтобы он выглядел как настоящий «виртуальный» метод шаблона.


Я придумал это решение, решая проблему на работе. Это похоже на приведенное выше описание Марка Эсселя, но, я надеюсь, лучше реализовано и объяснено.
sad1raf

Я бы уже квалифицировал это как обфускацию кода, и вы все еще не понимаете, что вам нужно модифицировать исходный Baseкласс каждый раз, когда вам нужно вызвать функцию шаблона с типом аргумента, несовместимым с теми, которые были реализованы до сих пор. Избежание этой необходимости - это намерение шаблонов ...
Аконкагуа

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

2

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

// abstract.h

// Simply define the types that each concrete class will use
#define IMPL_RENDER() \
    void render(int a, char *b) override { render_internal<char>(a, b); }   \
    void render(int a, short *b) override { render_internal<short>(a, b); } \
    // ...

class Renderable
{
public:
    // Then, once for each on the abstract
    virtual void render(int a, char *a) = 0;
    virtual void render(int a, short *b) = 0;
    // ...
};

Итак, теперь, чтобы реализовать наш подкласс:

class Box : public Renderable
{
public:
    IMPL_RENDER() // Builds the functions we want

private:
    template<typename T>
    void render_internal(int a, T *b); // One spot for our logic
};

Преимущество здесь в том, что при добавлении нового поддерживаемого типа все это можно сделать из абстрактного заголовка и, возможно, отказаться от него в нескольких файлах исходного кода / заголовка.


0

По крайней мере, в gcc 5.4 виртуальные функции могут быть членами шаблона, но должны быть самими шаблонами.

#include <iostream>
#include <string>
class first {
protected:
    virtual std::string  a1() { return "a1"; }
    virtual std::string  mixt() { return a1(); }
};

class last {
protected:
    virtual std::string a2() { return "a2"; }
};

template<class T>  class mix: first , T {
    public:
    virtual std::string mixt() override;
};

template<class T> std::string mix<T>::mixt() {
   return a1()+" before "+T::a2();
}

class mix2: public mix<last>  {
    virtual std::string a1() override { return "mix"; }
};

int main() {
    std::cout << mix2().mixt();
    return 0;
}

Выходы

mix before a2
Process finished with exit code 0

0

Попробуй это:

Напишите в classeder.h:

template <typename T>
class Example{
public:
    T c_value;

    Example(){}

    T Set(T variable)
    {
          return variable;
    }

    virtual Example VirtualFunc(Example paraM)
    {
         return paraM.Set(c_value);
    }

Проверьте, если вы работаете с этим, чтобы написать этот код в main.cpp:

#include <iostream>
#include <classeder.h>

int main()
{
     Example exmpl;
     exmpl.c_value = "Hello, world!";
     std::cout << exmpl.VirtualFunc(exmpl);
     return 0;
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.