Урок №125. статические методы класса

Отдельные базы для shared_ptr- и unique_ptr-поведения

Итак, у нас есть класс, который хранит указатель на сообщение. Теперь мы можем определить его наследников, которые и будут вести себя либо как shared_ptr, либо как unique_ptr.

Начнем со случая shared_ptr-поведения, т.к. здесь меньше всего кода:

Ничего сложного: наследуемся от , наследуем все его конструкторы и определяем простую, неразрушающую реализацию .

Для случая unique_ptr-поведения кода побольше, хотя сложного в нем ничего нет:

Опять же, наследуемся от и наследуем у него нужные нам конструкторы (это конструктор по-умолчанию и инициализирующий конструктор). Но при этом определяем конструкторы и операторы копирования/перемещения в соответствии с логикой unique_ptr: копирование запрещаем, перемещение реализуем.

Также у нас здесь разрушающий метод .

Вот, собственно, все. Осталось только реализовать выбор между двумя этими базовыми классами…

Выбор между shared_ptr- и unique_ptr-поведением

Для выбора между shared_ptr- и unique_ptr-поведением потребуется следующая метафункция (метафункция она потому, что «работает» с типами в компайл-тайм):

Эта метафункция принимает оба параметра из списка параметров и в качестве результата (т.е. определения вложенного типа ) «возвращает» тип, от которого следует отнаследоваться. Т.е. либо , либо .

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

А для того, чтобы использовать метафункцию было проще, следом определим более короткое имя для нее:

Преобразование типов данных в C#

Преобразование типов данных, это приведение одного типа к другому. Например, приведение целочисленной переменной к числу с плавающей точной, или преобразование числа в строку. В C# выделяют два варианта преобразования типов:

  • Неявное преобразование типов. Это, так называемое безопасное преобразование типов в C#. Например, преобразование из типа float (более «маленький» тип) в тип double (более «большой» тип). При таком преобразовании никакая информация не «потеряется», что при обратном преобразовании вполне возможно.
  • Явное преобразование типов. Такое преобразование выполняется программистом с прямым указанием типа, к которому нужно привести переменную. Для такого преобразования требуется наличие оператора преобразования.

А теперь, я покажу как можно использовать явное преобразование типов в C#:

using System;

namespace TypesConvertion
{
    class Program
    {
        static void Main(string[] args)
        {
            //Число с плавающей точкой
            double doubleData = 245.45;

            //Вывод этого числа 
            Console.WriteLine(doubleData);

            //А теперь, мы сделаем на основе этого числа, цело число
            int intData = (int)doubleData; 

            //Вывод целого числа 
            Console.WriteLine(intData);

            //Чтобы окно не закрылось раньше времени
            Console.ReadKey();
        }
    }
}

Преобразование типа double в тип int выполнялось в выделенной строке (строке с номером 16). Если вы соберете данный пример и запустите его, то увидите, что преобразование типов прошло, т.е. из числа с плавающей точкой мы получили целое число.

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

Название метода Целевой тип
ToBoolean bool
ToByte byte
ToChar char
ToDouble double
ToSingle float
ToInt32 int
ToInt64 long
ToString string

А теперь, пример использования класса Convert и его методов:

using System;

namespace TypesConvertion
{
    class Program
    {
        static void Main(string[] args)
        {
            //Число с плавающей точкой
            double doubleData = 245.45;

            //Вывод этого числа 
            Console.WriteLine(doubleData);

            //А теперь, мы сделаем на основе этого числа, цело число
            int intData = Convert.ToInt32(doubleData);

            //Вывод целого числа 
            Console.WriteLine(intData);

            //Чтобы окно не закрылось раньше времени
            Console.ReadKey();
        }
    }
}

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

Пользоваться возможность преобразования типов вам придется довольно часто!

2 Отличие статических и нестатических переменных

Нестатические (обычные) переменные класса объявляются точно так же, как статические, только без ключевого слова .

Чем же отличаются обычные и статические переменные?

Обычные переменные класса привязаны к объектам своего класса (экземплярам класса), статические переменные — к статическому объекту класса.

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

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

Пример:

Обращение к полю класса с использованием ссылки на объект класса

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

Пример:

Обращение к статическому полю класса не используя ссылку на объект класса

Устройство в памяти:

Допустим, у нас есть класс с 4 полями: два статических, а два — нет.

Сразу после загрузки класса

Когда Java-машина завершит загрузку класса , в памяти у нас будет наблюдаться такая картина:

После создания первого объекта

Если мы создадим объект класса , картинка станет такой

Обратите внимание, что хоть у объектов по две переменные, это разные переменные: у обычного объекта — обычные, у статического — статические. Нужно больше объектов

Нужно больше объектов

Давайте создадим еще два объекта, типа . Новая картина будет выглядеть так:

Обратите внимание: у каждого объекта есть собственная переменная age и name

Nginx¶

См.также

http://nginx.org/ru/docs/beginners_guide.html#static

Для примера возьмем следующую структуру файлов:

/usr/share/nginx/html/
|-- index.html
|-- index2.html
`-- static_example
    `-- static
        |-- html-css-js.png
        |-- jquery.min.js
        |-- script.js
        `-- style.css

2 directories, 6 files

страница, которая ссылается на другие статические файлы.

index2.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Untitled Document</title>
  <link href="/static/style.css" media="all" rel="stylesheet" type="text/css" />
  <script src="/static/jquery.min.js"></script>
  <script src="/static/script.js"></script>
</head>
<body>
<table width="100%" border="1" cellspacing="10" cellpadding="10">
  <tr>
    <td width="60%">
    <h1>HTML</h1>
    <a href="">HTML</a>
    <a href="">JS</a>
    <a href="" style="color: green">CSS</a>
    <hr/>
    <p class="replace-text">HTML (от англ. HyperText Markup Language — «язык
    гипертекстовой разметки») — стандартный язык разметки документов во
    Всемирной паутине. Большинство веб-страниц содержат описание разметки на
    языке HTML (или XHTML). Язык HTML интерпретируется браузерами и
    отображается в виде документа в удобной для человека форме.</p> <p
    style="color: black; background-color: red; color: #fff"> Язык HTML
    является приложением («частным случаем») SGML (стандартного обобщённого
    языка разметки) и соответствует международному стандарту ISO 8879.</p>
    <p class="hide"> Язык XHTML является более строгим вариантом HTML, он
    следует всем ограничениям XML и, фактически, XHTML можно воспринимать
    как приложение языка XML к области разметки гипертекста.</p> <p> Во
    всемирной паутине HTML-страницы, как правило, передаются браузерам от
    сервера по протоколам HTTP или HTTPS, в виде простого текста или с
    использованием сжатия.</p>
    <hr/>
    </td>
    <td width="40%"><img src="/static/html-css-js.png" class="jquery-image"
      width="500" height="293" alt="HTML JS CSS"></td> </tr>
</table>
</body>
</html>

Сервер Nginx настроен таким образом, что по адресу отдается
страница , а по файлы из директории
.

/etc/nginx/sites-enabled/default.nginx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# default.nginx

server {
    listen 80 default_server;

    root /usr/share/nginx/html;
    index index.html index.htm;

    include includes/fcgi.nginx;
    include includes/static.nginx;
}

/etc/nginx/includes/static.nginx

1
2
3
4
5
6
7
location  /example {
    try_files $uri $uri/ /index2.html;
}

location /static {
    alias /usr/share/nginx/html/static_example/static;
}

Пример index2.html без статики

Если скопировать файлы статики в директорию
, то сервер начнет их отдавать:

Наследование статических методов

Но как будут
вести себя статические методы при наследовании классов? Предположим, что мы
хотим добавить еще одного специализированного пользователя Admin:

class Admin extends Users {
         constructor(name, old, login, psw) {
                   super(name, old);
                   this.login = login;
                   this.psw = psw;
         }
}

Мы расширяем
базовый класс Users и добавляем еще
два свойства: login и psw. Будет ли
статический метод compareOld доступен в дочернем классе Admin? Да, будет и,
далее, мы можем создать такого пользователя:

let u2 = new Admin("Федор", 19, "aaa", "0123");

и сравнить их,
вызывая статический метод через класс Admin:

console.log( Admin.compareOld(u1, u2) );

То есть, метод compareOld можно вызывать
и через класс Users и через класс Admin. Разницы
никакой не будет. Это происходит по той причине, что свойство __proto__ класса Admin ссылается на
класс Users:

Если же добавить
еще один статический метод, но уже в класс Admin:

         static createAdmin(name, old) {
                   return new this(name, old, "admin", "root");
         }

то картина будет
такой:

В методе createAdmin мы создаем
нового пользователя Admin с использованием только двух
параметров: name, old. Остальные два
задаются по умолчанию как: «admin», «root». Причем,
ключевое слово this здесь будет ссылаться на класс, указанный перед
точкой, при вызове данного метода. Например:

let u3 = Admin.createAdmin("Сергей", 33);

Здесь this ссылается на Admin, поэтому будет
создан новый объект класса Admin. А вот если мы в методе compareOld
добавим вывод:

console.log(this == Admin);

и вызовем его
двумя способами:

Users.compareOld(u1, u2);  // false
Admin.compareOld(u1, u2);  // true

то в первом
случае this будет ссылаться
на Users, а во втором –
на Admin.

Объявление и определение переменной.

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

#include <conio.h> #include <stdio.h> int Global; //Объявили переменную int Global = 20; //Определили переменную void main() { printf(«%d», Global); getch(); }

Теперь, что будет, если одновременно объявить переменную и инициализировать её. Это определение переменной, которое требует её объявления

int Global = 20;

Следующая программа не скомпилируется

#include <conio.h> #include <stdio.h> extern int Global; void main() { Global = 30; printf(«%d», Global); getch(); }

Это связано с тем, что отсутствует определение переменной. Если определить переменную внутри main, то это будет уже другой экземпляр переменной, которая будет расположена на стеке. Вообще, при работе с одним файлом использование extern переменных не оправдано. Рассмотрим ситуацию, когда у нас имеются ещё два файла – заголовочный File1.h и File1.c. В заголовочном файле объявим extern переменную Global

#ifndef _FILE1_H_ #define _FILE1_H_ extern int Global; #endif

в файле исходного кода определим её

#include «File1.h» int Global = 100;

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

#include <conio.h> #include <stdio.h> #include «File1.h» void main() { printf(«%d\n», Global); getch(); }

Если теперь определим функцию, которая изменяет эту переменную, то все функции из всех файлов будут видеть эти изменения.

#ifndef _FILE1_H_ #define _FILE1_H_ #include <stdio.h> extern int Global; void changeAndPrint(); #endif #include «File1.h» int Global = 100; void changeAndPrint() { printf(«from File1: Global = %d\n», Global); Global = 1234; printf(«changed to %d\n», Global); } #include <conio.h> #include <stdio.h> #include «File1.h» void main() { Global = 567; printf(«From main: Global = %d\n», Global); changeAndPrint(); printf(«From main: Global = %d\n», Global); getch(); }

Вывод

База для getter-ов

Итак, у нас уже есть возможность выбрать базу, которая содержит указатель и определяет поведение «умного указателя». Теперь нужно снабдить эту базу методами-getter-ами. Для чего нам потребуется один простой класс:

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

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

В качестве параметра Return_Type будет выступать тип сообщения, указатель/ссылку на который будет возвращаться getter-ами. Фокус в том, что для иммутабельного сообщения типа параметр Return_Type будет иметь значение . Тогда как для мутабельного сообщения типа параметр Return_Type будет иметь значение . Таким образом метод для иммутабельных сообщений будет возвращать , а для мутабельных — просто .

Посредством свободной функции решается проблема работы с сообщениями, которые не отнаследованны от :

Т.е. если сообщение не наследуется от и хранится как , то вызывается вторая перегрузка. А если наследуется, то первая перегрузка.

Выбор конкретной базы для getter-ов

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

Обратить внимание можно разве что на вычисление параметра Return_Type. Один из тех немногих случаев, когда east const оказывается полезен ;). Ну и, для повышения читабельности последующего кода, более компактный вариант для работы с ней:

Ну и, для повышения читабельности последующего кода, более компактный вариант для работы с ней:

3 Удаление и добавление модификатора static

Из статической в обычную

Что будет, если мы возьмем статическую переменную и превратим ее в обычную: удалим у нее модификатор ? Например, у переменной .

Измененный код будет выглядеть так:

А в памяти мы получим такую картину:

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

Из обычной в статическую

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

А в памяти мы получим уже такую картину:

От чего отнаследован тип сообщения?

Типы сообщений, которыми параметризуется , делятся на две группы. Первая группа — это сообщения, которые наследуются от специального базового типа . Например:

В этом случае message_holder_t внутри себя должен содержать только указатель на объект этого типа. Этот же указатель должен возвращаться в методах-getter-ах. Т.е., для случая наследника от должно быть что-то вроде:

Вторая группа — это сообщения произвольных пользовательских типов, которые не наследуются от . Например:

Экземпляры таких типов в SObjectizer-е отсылаются не сами по себе, а заключенными в специальную обертку , которая уже наследуется от . Поэтому для таких типов должен содержать внутри себя указатель на , а методы-getter-ы должны возвращать указатель на M:

Статический членыStatic Members

Нестатический класс может содержать статические методы, поля, свойства или события.A non-static class can contain static methods, fields, properties, or events. Статический член вызывается для класса даже в том случае, если не создан экземпляр класса.The static member is callable on a class even when no instance of the class has been created. Доступ к статическому члены всегда выполняется по имени класса, а не экземпляра.The static member is always accessed by the class name, not the instance name. Существует только одна копия статического члена, независимо от того, сколько создано экземпляров класса.Only one copy of a static member exists, regardless of how many instances of the class are created. Статические методы и свойства не могут обращаться к нестатическим полям и событиям в их содержащем типе, и они не могут обращаться к переменной экземпляра объекта, если он не передается явно в параметре метода.Static methods and properties cannot access non-static fields and events in their containing type, and they cannot access an instance variable of any object unless it’s explicitly passed in a method parameter.

Более привычно объявление нестатического класса с несколькими статическими членами, чем объявление всего класса как статического.It is more typical to declare a non-static class with some static members, than to declare an entire class as static. Статические поля обычно используются для следующих двух целей: хранение счетчика числа созданных объектов или хранение значения, которое должно совместно использоваться всеми экземплярами.Two common uses of static fields are to keep a count of the number of objects that have been instantiated, or to store a value that must be shared among all instances.

Статические методы могут быть перегружены, но не переопределены, поскольку они относятся к классу, а не к экземпляру класса.Static methods can be overloaded but not overridden, because they belong to the class, and not to any instance of the class.

Несмотря на то, что поле не может быть объявлено как , поле const по своему поведению является статическим.Although a field cannot be declared as , a const field is essentially static in its behavior. Он относится к типу, а не к экземплярам типа.It belongs to the type, not to instances of the type. Поэтому к полям можно обращаться с использованием той же нотации , что и для статических полей.Therefore, fields can be accessed by using the same notation that’s used for static fields. Экземпляр объекта не требуется.No object instance is required.

C# не поддерживает статические локальные переменные (то есть переменные, объявленные в области действия метода).C# does not support static local variables (that is, variables that are declared in method scope).

Для объявления статических методов класса используется ключевое слово перед возвращаемым типом члена, как показано в следующем примере:You declare static class members by using the keyword before the return type of the member, as shown in the following example:

Статические члены инициализируются перед первым доступом к статическому члену и перед вызовом статического конструктора, если таковой имеется.Static members are initialized before the static member is accessed for the first time and before the static constructor, if there is one, is called. Для доступа к члену статического класса следует использовать имя класса, а не имя переменной, указывая расположение члена, как показано в следующем примере:To access a static class member, use the name of the class instead of a variable name to specify the location of the member, as shown in the following example:

Если класс содержит статические поля, должен быть указан статический конструктор, который инициализирует эти поля при загрузке класса.If your class contains static fields, provide a static constructor that initializes them when the class is loaded.

Вызов статического метода генерирует инструкцию вызова в промежуточном языке Microsoft (MSIL), в то время как вызов метода экземпляра генерирует инструкцию , которая также проверяет наличие ссылок на пустые объекты.A call to a static method generates a call instruction in Microsoft intermediate language (MSIL), whereas a call to an instance method generates a instruction, which also checks for null object references. Однако в большинстве случаев разница в производительности двух видов вызовов несущественна.However, most of the time the performance difference between the two is not significant.

1 Статические методы

Кроме статических переменных, в классах могут быть и статические методы.

Обычные методы привязаны к объектам (экземплярам) класса и могут обращаться к обычным-переменным класса (а также к статическим переменным и методам). Статические же методы привязаны к статическому объекту класса и могут обращаться только к статическим переменным и/или другим статическим методам класса.

Чтобы вызвать обычный метод у класса, сначала нужно создать объект этого класса, а только потом вызвать метод у объекта. Вызвать обычный метод не у объекта, а у класса нельзя.

Пример:

Вызвать нестатический метод у класса нельзя!

А чтобы вызвать статический метод, достаточно чтобы просто существовал статический объект класса (который всегда существует после загрузки класса в память). Именно поэтому метод main() — статический. Он привязан к статистическому объекту класса, для его вызова не нужно создавать никакие объекты.

Чтобы объявить метод статическим, нужно перед заголовком метода написать ключевое слово static. Общий вид этой конструкции такой:

Примеры:

Код Примечание
Метод вызывается Java-машиной командой вида: ;
Статический метод вызывается в статическом методе .

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

Примеры:

Код Статический метод

Что такое статические методы в Java?

Статические методы — это методы в Java, которые можно вызывать без создания объекта класса. Они задокументированы именем {class the category}. Статическое ключевое слово может использоваться с классом, переменной, методом и блоком. Статические члены принадлежат классу, а не конкретному экземпляру, это означает, что если вы сделаете член статическим, вы сможете получить к нему доступ без объекта. Давайте рассмотрим пример, чтобы понять это:

Здесь у нас есть статический метод myMethod(), мы можем вызвать этот метод без какого-либо объекта, потому что когда мы делаем член статическим, он становится уровнем класса. Если мы удалим ключевое слово static и сделаем его нестатичным, нам нужно будет создать объект класса для его вызова.

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

class SimpleStaticExample
{
    // This is a static method
    static void myMethod()
    {
        System.out.println("myMethod");
    }
 
    public static void main(String[] args)
    {
          /* You can see that we are calling this
           * method without creating any object. 
           */
           myMethod();
    }
}

Синтаксис

public static void geek(String name)
{
// code to be executed....

Он хранится в Permanent Generation, поскольку связывается с {class the category}, где они находятся, а не с объектами этого класса. Тем не менее, их локальные переменные, а также передаваемый им аргумент(ы) находятся в стеке.

Важные моменты:

  • Статический метод(ы), связанный с классом, в котором они находятся, то есть они будут ссылаться на него, даже если он не создает экземпляр класса, т.е. ClassName.methodName (args).
  • Они предназначены для совместного использования всеми объектами, созданными из одного класса.
  • Статические методы не могут быть переопределены.

Пример использования статических методов в Java:

import java.io.*;
class Flair{
   public static String FlairName = "";
   public static void geek(String name)
{
         FlairName = name;
   }
}
class GFG {
   public static void main (String[] args) {
         Flair.flair("vaibhav");
         System.out.println(Flair.flairName);
         Flair obj = new Flair ();
         obj.flair("shadow");
         System.out.println(obj.flairName);
   }
}

Вывод:

Что если статическая переменная ссылается на объект?

В первой строке значение, которое будет храниться в разделе PermGen. Во второй строке ссылка obj будет храниться в секции PermGen, а объект, на который она ссылается, будет храниться в секции heap.

ПримерExample

В этом примере класс имеет статический конструктор.In this example, class has a static constructor. При создании первого экземпляра класса () для инициализации класса вызывается статический конструктор.When the first instance of is created (), the static constructor is invoked to initialize the class. В выходных данных этого примера можно увидеть, что статический конструктор выполняется только один раз, несмотря на то, что создается два экземпляра класса . Кроме того, этот конструктор вызывается до выполнения конструктора экземпляра.The sample output verifies that the static constructor runs only one time, even though two instances of are created, and that it runs before the instance constructor runs.

Итоговый наследник message_holder_t

Теперь можно посмотреть на то, что же из себя представляет , для реализации которого потребовались все эти базовые классы и метафункции (из реализации удалена часть методов для конструирования экземпляра хранящегося в message_holder-е сообщения):

По сути все то, что мы разбирали выше, потребовалось для того, чтобы записать вот этот «вызов» двух метафункций:

Т.к. это не первый вариант, а результат упрощения и сокращения кода, то могу сказать, что компактные формы метафункций ну очень сильно уменьшают объем кода и увеличивают его понятность (если о понятности здесь вообще уместно говорить).

А что было бы, если бы…

А вот если бы в C++ был настолько же мощен, как в D, то можно было бы написать что-то вроде:

Как по мне, так отличия слишком уж разительны. И они не в пользу текущего C++ 🙁
(разобранный выше C++ный код в виде одной сплошной «портянки» можно увидеть здесь).

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

Заключение

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

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

Тем не менее, сам факт того, что на С++ можно такое сотворить, меня лично радует. Огорчает количество труда и объем кода, который для этого потребуется. Но, надеюсь, что со временем объем этого кода и его сложность будет только сокращаться. В принципе, это видно уже сейчас. Ибо для C++98/03 я даже не взялся бы такой трюк проделывать, тогда как начиная с C++11 делать подобное становится все проще и проще.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector