С момента моего ознакомительного исследования C# прошло полтора года. Поработав с языком, я убедился в превосходстве C++ над ним. Когда я говорю об этом, мои оппоненты часто начинают приводить доводы, касательно того, как хорош .NET или, что архитектура CLR накладывает ограничения на язык. Ни тот, ни другой аргументы не могут оправдать C#, так как язык — это то, что программисты видят каждый день, а CLR скрыт от глаз и не должен оказывать влияния на инструмент разработки, как архитектура процессора не оказывает влияния на C++. В этой статье я не буду обсуждать стандартную библиотеку и рантайм, речь пойдёт только о языке.
Не затягивая со вступлением, начну с самых основ.
Якщо вам потрібен якісний сайт або портал, то рекомендуємо звернутися у компанію  web-ukraine, вони допоможуть усе зробити по вашим бажанням, створення сайтів, підтримка сайтів, реклама сайтів все це входить у вартість послуг.

ООП ради ООП

Программируя долгое время на C++, я не задумывался о минусах ОО подхода. Один мой знакомый постоянно спорил со мной, утверждая, что ООП — это зло. Тогда я не понимал истинных причин его выводов. Лишь начав программировать на C#, я осознал всю несостоятельность объектно-ориентированной парадигмы. Дело в том, что пока я писал на C++, я активно пользовался процедурной парадигмой и ООП лишь помогало там, где оно имело место. В C# (как впрочем и в Java) объектно-ориентированная парадигма вытеснила все другие. В итоге в C# мы имеем множество статических классов-Helper`ов и Utility. Гипертрофия ООП в C# приводит к тому, что классы содержат невероятно избыточное количество данных для настройки своего поведения и поведения своих потомков. Подробнее о недостатках ООП подхода можно почитать здесь. Приведу одну цитату:

Единственный способ сделать повторно используемые бизнес компоненты на этом уровне — сделать их сверх-настраиваемыми путём добавления таких штук как движки правил и встраиваемые языки. Такую модель я бы не стал называть моделью компонента. Скорее моделью очередного экземпляра bloatware. Обещание о повторном использовании ушло, люди либо покупали огромные bloatware системы(неудачники), или разрабатывали свои специальные бизнес объекты в своих собственных проектах.

Кроме того, Вы можете ознакомиться со знаменитой статьёй Ричарда Гэбриэла «Объектная парадигма провалилась» и её триумфальное возрождением «Почему объектно-ориентированное программирование провалилось?». Советую обратить внимание ещё и на эту статью.
Подводя итог вышеперечисленным статьям, можно сказать, что концепция ООП не справилась с возложенными на неё ожиданиями, вместо этого она усложнила разработку и архитектуру приложений. C++ же не настаивает на использовании ООП и позволяет получить от него максимум выгоды, используя там, где это оправдано. Доклад Джека Дидриха довольно точно описывает моё отношение к ООП: не нужно писать класс, если можно написать функцию.
Исходя из того, что C# ничего, кроме ООП не предлагает, в дальнейшем я буду рассматривать C# и C++ с точки зрения объектно-ориентированной парадигмы.

Наследование

Наследование в C++ — это одна из самых широких возможностей. C++ позволяет использовать один из нижеприведённых вариантов или любую их комбинацию:

  • Наследование интерфейса;
  • Наследование реализации;
  • Множественное наследование;
  • Виртуальное наследование.

C# изо всего этого поддерживает лишь наследование интерфейса, наследование интерфейса вместе с реализацией и множественное наследование интерфейсов без реализации.
Вообще говоря, множественное наследование интерфейсов с реализациями используется довольно редко, а виртуальное ещё реже. Но при возникновении такой необходимости в C# приходится применять сложные обходные пути. К сожалению, реальные примеры слишком сложны, а книжные мало убедительны, тем не менее, я постараюсь показать полезность множественного наследования на небольшом примере.
Предположим, что в программе имеется класс Clock (часы) и класс Phone (телефон).

class Clock
{
public:
    unsigned int hours() const;
    unsigned int minutes() const;
    unsigned int seconds() const;
};

class Phone
{
public:
    PhoneStream * call(const std::string & number);
};

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

class CellPhone : public Clock, public Phone { };

Вместо одной строки кода, на C# придётся написать

class CellPhone {
    Phone phone = new Phone();
    Clock clock = new Clock();

    public Phone Phone {
        get { return phone; }
    }

    public Clock Clock {
        get { return clock; }
    }
}

При такой реализации клиенты этого класса будут вынуждены углубить связность с его внутренней реализацией, что противоречит закону Деметры. Чтобы избежать связности можно реализовать все методы классов Clock и Phone внутри класса CellPhone, но это имеет смысл только при условии, что указанные классы имеют интерфейсы и используются только через них.
Теперь усложним задачу. Каждый класс, Clock и Phone, будут унаследованы от класса Device (устройство).

class Device
{
public:
    void powerOn();
    void powerOff();
};

class Clock : public Device
{
public:
    unsigned int hours() const;
    unsigned int minutes() const;
    unsigned int seconds() const;
};

class Phone: public Device
{
public:
    PhoneStream * call(const std::string & number);
};

class CellPhone : public Clock, public Phone { };

В этом коде есть одна, не слишком очевидная, проблема. Объекты класса CellPhone будут иметь два экземпляра класса Device: один от Clock, другой от Phone. Для таких случаев (которые получили название ромбовидное наследование) в C++ введён специальный тип наследования — виртуальное наследование.

class Device
{
public:
    void powerOn();
    void powerOff();
};

class Clock : public virtual Device
{
public:
    unsigned int hours() const;
    unsigned int minutes() const;
    unsigned int seconds() const;
};

class Phone: public virtual Device
{
public:
    PhoneStream * call(const std::string & number);
};

class CellPhone : public Clock, public Phone { };

Теперь объекты CellPhone будут иметь один экземпляр класса Device.
C# не предоставляет подобной возможности. В агрегированных объектах у Вас всегда будут храниться два разных объекта класса Device, бездарно расходуя память.
Подробно о пользе и вреде множественного наследования  Вы можете прочитать в книге Скотта Майерса «Эффективное использование C++. 50 рекомендаций по улучшению Ваших программ и проектов» в правиле 42: «Продумывайте подход к использованию множественного наследования».
Более реальный пример множественного наследования  — реализация паттерна проектирования Observer (наблюдатель).

#include <iostream>
#include <set>

template<typename T>
class Observable
{
protected:
    Observable() { }
    virtual ~Observable() { }

public:
    bool addObserver(T * observer)
    {
        return m_observers.insert(observer).second;
    }

    bool removeObserver(T * observer)
    {
        return m_observers.erase(observer);
    }

protected:
    std::set<T *> m_observers;
};

class Worker
{
public:
    virtual ~Worker() { }
    virtual void start() = 0;
    virtual void stop() = 0;
};

class WorkerObserver
{
public:
    virtual ~WorkerObserver() { }
    virtual void onStart() = 0;
    virtual void onStop() = 0;
};

class ObservableWorker : public Observable<WorkerObserver>, public Worker
{
public:
    virtual void start()
    {
        for(std::set<WorkerObserver *>::iterator it = m_observers.begin();
            m_observers.end() != it; ++it)
        {
            (*it)->onStart();
        }
    }

    virtual void stop()
    {
        for(std::set<WorkerObserver *>::iterator it = m_observers.begin();
            m_observers.end() != it; ++it)
        {
            (*it)->onStop();
        }
    }
};

class WorkerLogger : public WorkerObserver
{
public:
    virtual void onStart()
    {
        std::cout << "Started" << std::endl;
    }

    virtual void onStop()
    {
        std::cout << "Stoped" << std::endl;
    }
};


int main()
{
    ObservableWorker worker;
    WorkerLogger logger;
    worker.addObserver(&logger);
    worker.start();
    worker.stop();
    std::cin.get();
    return 0;
}

В данном подходе класс ObservableWorker является одновременно и Worker`ом и Observable`ом. Без множественного наследования пришлось бы выбирать между родителями, и выбор был бы в пользу Worker`а, а методы addObserver и removeObserver пришлось бы реализовавать в каждом классе. Кроме упомянутых методов, класс Observable мог бы содержать дополнительные методы работы с коллекцией наблюдателей, но для краткости я не стал раздувать код примера.

Ещё один вид наследования, который оказался за бортом C# — наследование реализации без интерфейса или, проще говоря, приватное наследование. Этот вид наследования не делает унаследованный класс потомком родителя и не может участвовать в его полиморфных операциях. По большому счёту, закрытое наследование мало отличается от реализации класса посредством агрегации объекта некоторого другого класса (того, от которого производится наследование). Но в закрытом наследовании есть один большой плюс: оно предоставляет доступ ко всем защищённым членам базового класса. Один из вариантов использования закрытого наследования приведён в книге Скотта Майерса «Эффективное использование C++. 50 рекомендаций по улучшению Ваших программ и проектов» в правиле 42: «Продумывайте подход к использованию закрытого наследования». Этот пример довольно интересен, но является специфичным для C++, поэтому тут я его приводить не буду.

Инкапсуляция

Один из самых любимых мною, да и всеми C++ программистами, паттерн Private Implementation (например, библиотека Qt очень активно использует этот паттерн) не применим в C# вообще. Причиной этому является открытость всех модулей. В C++, желая скрыть детали имплементации класса, мы можем предварительно объявить в заголовочном файле приватный внутренний класс и указатель на него, а сам интерфейс и реализацию перенести в *.cpp модуль. Тем самым мы скрываем все переменные и дополнительные, вспомогательные методы, которые мы используем, от посторонних глаз. Интерфейс остаётся чистым и понятным при реализации любой сложности. В описанной стратегии очень сложно достучаться до деталей приватной имплементации. В C# при помощи reflection можно вызвать любой приватный метод и изменить приватное состояние объекта всего в одну-две строки кода. Но этот соблазн ещё выше с учётом того, что все внутренности класса находятся в одном модуле.

Объявление синонимов типа

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

Tuple<Dictionary<string, List<MyObject>>, bool>

Вместо того, чтобы всякий раз писать эту длинную последовательность, оператор typedef позволяет сократить запись. В C# это могло бы выглядеть так

typedef Tuple<Dictionary<string, List<MyObject>>, bool> MyType;

И далее, везде, где использовался изначальный длинный тип, можно было бы писать просто MyType.
Кроме удобочитаемого имени, оператор typedef используется в C++ для получения типов из других классов или шаблонов. Приведу простой пример использования.

#include <iostream>
#include <vector>

struct Letter
{
    typedef char ValueType;
    char value;
};

struct Number
{
    typedef int ValueType;
    int value;
};

template<typename Element>
class Container
{
public:
    typedef typename Element::ValueType  ElementValueType;

    void add(ElementValueType value)
    {
        Element element;
        element.value = value;
        m_elements.push_back(element);
    }

    void print()
    {
        for(typename std::vector<Element>::const_iterator it = m_elements.begin();
            m_elements.end() != it; ++it)
        {
            std::cout << it->value << " ";
        }
    }
    
private:
    std::vector<Element> m_elements;
};


void testLetter()
{
    Container<Letter> container;
    container.add('a');
    container.add('b');
    container.add('c');
    std::cout << "Letters: ";
    container.print();
    std::cout << std::endl;
}

void testNumber()
{
    Container<Number> container;
    container.add(1);
    container.add(2);
    container.add(3);
    std::cout << "Numbers: ";
    container.print();
    std::cout << std::endl;
}

int main()
{
    testLetter();
    testNumber();
    std::cin.get();
}
Letters: a b c
Numbers: 1 2 3

Класс Container получает из типа шаблона тип значения, которое хранится в некотором другом классе и которое доступно из переменной value. Далее класс Container может оперировать этим типом, в том числе и в параметрах методов. Typedef`ы типов могут быть вложены и тогда классы Number и Letter могут стать одним шаблоном. Работают такие конструкции во время компиляции, что обеспечивает почти полную защиту от ошибок.
Более сложные примеры с большой вложенностью Вы сможете найти в заголовочных файлах контейнеров (std::vector, std::map итд) стандартной библиотеки шаблонов (STL) С++.

Обобщения

Одним из самых мощных инструментов в C++ являются шаблоны. Сразу приведу пример, с которым я недавно столкнулся. Представим, что у нас есть абстрактный класс Connection

class Connection
{
public:
    virtual ~Connection() { }
    virtual bool connect(const std::string & address, short port) = 0;
    virtual void disconnect() = 0;
    virtual bool isConnected() const = 0;
    virtual void send(char * buffer, size_t size) = 0;
    virtual size_t recv(char * buffer, size_t size) = 0;
};

И от него унаследованы два (или более) конкретных класса: TcpConnection и SshConnection

class TcpConnection : public Connection
{
public:
    inline TcpConnection();
    virtual bool connect(const std::string & address, short port);
    virtual void disconnect();
    virtual bool isConnected() const;
    virtual void send(char * buffer, size_t size);
    virtual size_t recv(char * buffer, size_t size);

private:
    bool m_is_connected;
};

TcpConnection::TcpConnection() :
    m_is_connected(false)
{
}

bool TcpConnection::connect(const std::string & address, short port)
{
    std::cout << "TcpConnection::connect" << std::endl;
    m_is_connected = ::rand() % 2;
    return m_is_connected;
}

void TcpConnection::disconnect()
{
    std::cout << "TcpConnection::disconnect" << std::endl;
    m_is_connected = false;
}

bool TcpConnection::isConnected() const
{
    std::cout << "TcpConnection::isConnected" << std::endl;
    return m_is_connected;
}

void TcpConnection::send(char * buffer, size_t size)
{
    std::cout << "TcpConnection::send" << std::endl;
}

size_t TcpConnection::recv(char * buffer, size_t size)
{
    std::cout << "TcpConnection::recv" << std::endl;
    return size;
}
class SshConnection : public Connection
{
public:
    inline SshConnection();
    virtual bool connect(const std::string & address, short port);
    virtual void disconnect();
    virtual bool isConnected() const;
    virtual void send(char * buffer, size_t size);
    virtual size_t recv(char * buffer, size_t size);

private:
    bool m_is_connected;
};

SshConnection::SshConnection() :
    m_is_connected(false)
{
}

bool SshConnection::connect(const std::string & address, short port)
{
    std::cout << "SshConnection::connect" << std::endl;
    m_is_connected = ::rand() % 2;
    return m_is_connected;
}

void SshConnection::disconnect()
{
    std::cout << "SshConnection::disconnect" << std::endl;
    m_is_connected = false;
}

bool SshConnection::isConnected() const
{
    std::cout << "SshConnection::isConnected" << std::endl;
    return m_is_connected;
}

void SshConnection::send(char * buffer, size_t size)
{
    std::cout << "SshConnection::send" << std::endl;
}

size_t SshConnection::recv(char * buffer, size_t size)
{
    std::cout << "SshConnection::recv" << std::endl;
    return size;
}

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

template<typename T>
class ConnectionAdapter : public T
{
public:
    virtual bool connect(const std::string & address, short port);
    virtual void disconnect();
};

template<typename T>
bool ConnectionAdapter<T>::connect(const std::string & address, short port)
{
    bool connected = T::connect(address, port);
    std::cout << "Log: " << (connected ? "Connected" : "Connection failed") << std::endl;
    return connected;
}

template<typename T>
void ConnectionAdapter<T>::disconnect()
{
    bool connected = T::isConnected();
    T::disconnect();
    std::cout << "Log: " << (connected ? "Disconnected" :
        "Doesn't disconnected because connection hasn't been established") << std::endl;
}

Теперь объекты класса ConnectionAdapter можно использовать точно так же, как и объекты типа его параметра.

void tcpFunction(TcpConnection & connection)
{
    connection.connect("example.com", 80);
    connection.recv(NULL, 0);
    connection.send(NULL, 0);
    connection.disconnect();
}

void sshFunction(SshConnection & connection)
{
    connection.connect("example.com", 22);
    connection.recv(NULL, 0);
    connection.send(NULL, 0);
    connection.disconnect();
}

void commonFunction(Connection & connection)
{
    connection.connect("example.com", 123);
    connection.recv(NULL, 0);
    connection.send(NULL, 0);
    connection.disconnect();
}


int main()
{
    ::srand(::time(NULL));

    ConnectionAdapter<TcpConnection> tcp_connection;
    ConnectionAdapter<SshConnection> ssh_connection;
    tcpFunction(tcp_connection);
    std::cout << std::endl;
    sshFunction(ssh_connection);
    std::cout << std::endl;
    commonFunction(tcp_connection);
    std::cout << std::endl;
    commonFunction(ssh_connection);
    std::cin.get();
    return 0;
}

Вы не сможете сделать нечто подобное на C#. C# не позволяет наследоваться от параметра обобщения. Таким образом, единственное, что Вам оставляет этот язык — унаследовать базовый класс, Connection, реализовать все его абстрактные методы при помощи агрегированного объекта конкретного класса, и дописать в двух нужных методах по паре строк кода. И даже сделав это, Вы не сможете использовать объекты данного класса как объекты конкретного класса. Кроме приведённого примера, есть ещё одна очень удобная возможность использовать подобный механизм в купе с приватным наследованием. Передавая в шаблон некоторый тип Вы изменяете реализацию для шаблонного класса, хотя, как уже было сказано, можно заменить это простой агрегацией. Другим серьёзным недостатком обобщений C# является невозможность специализации. Если Вы точно знаете, что для определённого типа данных Ваш шаблон будет работать крайне не оптимально, то Вы можете создать для этого типа отдельную реализацию. C++ позволяет специализировать как все, так и любое количество аргументов шаблона.

#include <iostream>
#include <string>

template<typename T1, typename T2>
class Tuple
{
public:
    Tuple(const T1 & p1, const T2 & p2) :
        m_p1(p1),
        m_p2(p2)
    {
    }

    void print()
    {
        std::cout << m_p1 << ", " << m_p2 << std::endl;
    }

private:
    T1 m_p1;
    T2 m_p2;
};

template<typename T>
class Tuple<T, int>
{
public:
    Tuple(const T & p1, const int & p2) :
        m_p1(p1),
        m_p2(p2)
    {
    }

    void print()
    {
        std::cout << "T, int specialization: " << m_p1 << ", " << m_p2 << std::endl;
    }

private:
    T m_p1;
    int m_p2;
};

template<>
class Tuple<std::string, int>
{
public:
    Tuple(const std::string & p1, const int & p2) :
        m_p1(p1),
        m_p2(p2)
    {
    }

    void print()
    {
        std::cout << "std::string, int specialization: " << m_p1 << ", " << m_p2 << std::endl;
    }

private:
    std::string m_p1;
    int m_p2;
};


int main()
{
    Tuple<std::string, std::string>("Hello", "World").print();
    Tuple<int, int>(100, 42).print();
    Tuple<std::string, int>("Hello", 42).print();
    std::cin.get();
    return 0;
}

Этот код даст на выходе такой результат

Hello, World
T, int specialization: 100, 42
std::string, int specialization: Hello, 42

Как видно, компилятор выбирает наиболее подходящую реализацию исходя из параметров шаблона. К сожалению, для специализации шаблонов необходимо полностью повторить интерфейс и реализацию шаблона. Но, имея такой мощный инструмент, как препроцессор, в C++ это не является серьёзной проблемой. Справедливости ради, стоит отметить, что обобщения C# строго типизированы и поддерживают ковариантность (out) и контравариантность (in) аргументов, что обеспечивает поддержку наследования. Для того, чтобы C++ поддерживал эти механизмы необходимо вручную писать операторы преобразования типов.

Препроцессор

Препроцессор в C и C++ является одним из важнейших механизмов генерации кода. Роль препроцессора в C# ограничена лишь условной компиляцией и выдачей предупреждений и ошибок во время компиляции. C и C++ предлагают механизмы генерации кода с помощью препроцессора. Скажем, приложение использует пользовательский тип исключений

class Exception
{
public:
    Exception() :
        m_id(0)
    {
    }
    
    explicit Exception(int id) :
        m_id(id)
    {
    }

    Exception(int id, const std::string & message) :
        m_id(id),
        m_message(message)
    {
    }

private:
    int m_id;
    std::string m_message;
};

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

#define DECLARE_EXCEPTION(name)                     \
    class name : public Exception                   \
    {                                               \
    public:                                         \
        name() { }                                  \
        name(int id) :                              \
            Exception(id)                           \
        {                                           \
        }                                           \
        name(int id, const std::string & message) : \
            Exception(id, message)                  \
        {                                           \
        }                                           \
    };

Всё, что нужно написать для определения двух классов — две строки.

DECLARE_EXCEPTION(NotFoundException)
DECLARE_EXCEPTION(InvalidArgumentException)

Современные IDE, включая Visual Studio, способны выдавать подсказки по сгенерированному таким образом коду.

Неизменяемость

C# ограничивает понятие константности только лишь статическими ссылками на примитивные типы (POD),  дополнительно вводит ключевое слово readonly для того, чтобы ссылке нельзя было переприсвоить объект. Этих механизмов не достаточно, чтобы обеспечить безопасную манипуляцию объектами и избавиться от side-эффектов. C++ предлагает механизм неизменяемых объектов (равно как и переменных POD типов). Если Вы объявляете переменную или параметр функции с модификатором const, то в дальнейшем Вы не можете ни переприсвоить этой переменной значение, ни вызвать из такого объекта метод, который может изменить этот объект. Кроме того, модификатор const может быть применён к типу возвращаемого значения функции. Таким образом, можно реализовать безопасный возврат ссылки или указателя с ограничением только на чтение. Методы, которые не меняют состояние объекта помечаются модификатором const в конце сигнатуры. Внутри константных методов все члены-данные класса неявным образом становятся константами, за исключением тех, что помечены специальным модификатором mutable. Модификатор mutable говорит, что данная переменная не оказывает влияние на состояние объекта и может быть изменена в константных методах. Примером такой переменной может быть счётчик обращений к какому-либо методу. Следующий код вполне валиден  для C#.

class Person {
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public override string ToString() {
        return String.Format("{0}, {1}", LastName, FirstName);
    }
}

class Account {
    readonly Person person;

    public Account(Person person) {
        this.person = person;
    }

    public Person Person {
        get { return person; }
    }
}

class Program {
    static void Main(string[] args) {
        Account account = new Account(new Person {
            FirstName = "John",
            LastName = "Connor"
        });
        account.Person.FirstName = "Isaac";
        account.Person.LastName = "Asimov";
        Console.Out.WriteLine(account.Person);
        Console.ReadKey();
    }
}

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

#include <string>

class Person
{
public:
    Person(const std::string & first_name, const std::string & last_name) :
        m_first_name(first_name),
        m_last_name(last_name)
    {
    }

    void setFirstName(const std::string & first_name)
    {
        m_first_name = first_name;
    }

    const std::string & firstName() const
    {
        return m_first_name;
    }

    void setLastName(const std::string & last_name)
    {
        m_last_name = last_name;
    }

    const std::string & lastName() const
    {
        return m_last_name;
    }

private:
    std::string m_first_name;
    std::string m_last_name;
};

class Account
{
public:
    Account(const Person & person) :
        m_person(person)
    {
    }

    const Person & person() const
    {
        return m_person;
    }

private:
    Person m_person;
};

int main()
{
    Account account(Person("John", "Connor"));
    account.person().setFirstName("Isaac"); // Ошибка
    account.person().setLastName("Asimov"); // Ошибка
    std::cin.get();
    return 0;
}

Строки, помеченные комментарием «Ошибка», к примеру в Visual Studio, приведут к такому сообщению компилятора:

…consoleapplication1.cpp(58): error C2662: ‘Person::setFirstName’ : cannot convert ‘this’ pointer from ‘const Person’ to ‘Person &’

На моей практике встречались случаи, когда getter`ы свойств меняли кучу переменных класса, создавая side-эффекты, которые приводили к длительной отладке. Если бы в C# поддерживались константные методы (и getter`ы свойств), то таких ситуаций практически не возникало.

Ссылки

В C++ существует два способа указывания на объект: классические указатели и ссылки. Указатели являются адресом переменной в памяти, а ссылки — это механизм, введённый в язык C++ с целью повышения удобства и безопасности. Между ссылками и указателями есть две принципиальных разницы:

  1. ссылке нельзя переприсвоить значение;
  2. ссылка не может быть нулевой.

Первое ограничение касается, скорее, только безопасности, а вот второе очень сильно помогает в работе. В C++ принято принимать в функцию указатель, если параметр может быть равным NULL, в противном случае (чаще всего) принимают  ссылку. В C# ссылки могут быть равными null, что приводит к постоянной проверке всех аргументов и частому возникновению NullReferenceException`ов. Да, в C# параметр можно пометить ключевым словом ref, тогда нельзя будет передать null явно, но следующий код будет вполне валидным.

class Program {
    static void PrintString(ref string str) {
        Console.WriteLine(str);
    }

    static void Main(string[] args) {
        string str = null;
        PrintString(ref str);
        Console.ReadKey();
    }
}

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

Копирование объектов

Паттерн проектирования prototype (прототип) предполагает наличие одного объекта (прототипа) и его копирования для создания новых объектов. Такой подход часто обусловлен дороговизной инициализации новых объектов. В языке C++ предусмотрены мощные средства для создания копий объектов — это конструкторы копирования и операторы присваивания. Конструкторы копирования создают новый объект по переданному образцу, а операторы присваивания заполняют существующий объект из образца. C++ никак не ограничивает программиста в реализации упомянутых методов. Если конструктор копирования или оператор присваивания не определены, то компилятор сгенерирует их автоматически. Копирование по умолчанию подразумевает полное побайтовое копирование объектов.

#include <iostream>
#include <cstring>

class Data
{
public:
    Data(void * data, size_t size);
    Data(const Data & data);
    ~Data();
    Data & operator = (const Data & data);
    inline const void * data() const;

private:
    void * mp_data;
    size_t m_size;
};

Data::Data(void * data, size_t size) :
    m_size(size)
{
    mp_data = ::malloc(size);
    ::memcpy(mp_data, data, size);
    std::cout << "Constructor" << std::endl;
}

Data::Data(const Data & data) :
    m_size(data.m_size)
{
    mp_data = ::malloc(m_size);
    ::memcpy(mp_data, data.mp_data, m_size);
    std::cout << "Copy constructor" << std::endl;
}

Data::~Data()
{
    ::free(mp_data);
    std::cout << "Destructor" << std::endl;
}

Data & Data::operator = (const Data & data)
{
    if(this ==  &data) return *this;
    ::free(mp_data);
    m_size = data.m_size;
    mp_data = ::malloc(m_size);
    ::memcpy(mp_data, data.mp_data, m_size);
    std::cout << "Assignment operator" << std::endl;
    return *this;
}

const void * Data::data() const
{
    return mp_data;
}

void test()
{
    char * content = "Hello, World!";

    Data data1(content, strlen(content) + 1); // Конструктор
    Data data2(data1); // Конструктор копирования
    Data data3(NULL, 0); // Конструктор
    data3 = data2; // Оператор присваивания

    std::cout << data1.data() << ": " << static_cast<const char *>(data1.data()) << std::endl;
    std::cout << data2.data() << ": " << static_cast<const char *>(data2.data()) << std::endl;
    std::cout << data3.data() << ": " << static_cast<const char *>(data3.data()) << std::endl;
}

int main()
{
    test();
    std::cin.get();
    return 0;
}
Constructor
Copy constructor
Constructor
Assignment operator
00489448: Hello, World!
0048A5D0: Hello, World!
0048A620: Hello, World!
Destructor
Destructor
Destructor

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

  • Запрет копирования объектов класса. Реализуется путём объявления конструктора копирования и оператора присваивания в приватной секции. При этом реализацию можно опустить, тогда при попытке использования вы получите ошибку от линкера.
  • Модификация копий объектов или статических переменных. Таким образом, к примеру, реализуются «умные» указатели с подсчётом ссылок.

C# при применении оператора копирования копирует лишь ссылку на объект (за исключением значимых типов данных).  Для копирования объектов предназначен интерфейс ICloneable. Этот интерфейс содержит лишь один метод Clone, который возвращает объект типа object. Из этого следует, что клонированный объект всегда нужно приводить к нужному типу (и надеяться на порядочность разработчиков класса).

class Data : ICloneable {
    private byte[] data;

    public Data(byte[] data) {
        this.data = new byte[data.Length];
        data.CopyTo(this.data, 0);
    }

    public byte[] Buffer {
        get { return data; }
    }

    public object Clone() {
        return new Data(data);
    }
}

unsafe class Program {
    public static void Main(string[] args) {
        byte[] content = Encoding.ASCII.GetBytes("Hello, World");
        Data data1 = new Data(content);
        Data data2 = data1.Clone() as Data;
        PrintData(data1);
        PrintData(data2);
        Console.ReadKey();
    }

    static void PrintData(Data data) {
        fixed(byte* ptr = data.Buffer) {
            Console.WriteLine("{0:x}: {1}", (int)ptr, Encoding.ASCII.GetString(data.Buffer));
        }
    }
}
232f454: Hello, World
232f478: Hello, World

Тем не менее, из примера видно, что подход C# справился с поставленной задачей. Но что будет, если необходимо унаследовать один из классов, к исходникам которого у Вас нет доступа и который не реализует ICloneable?

class Data {
    private byte[] data;

    public void Load(string url) {
        using(WebClient client = new WebClient()) {
            data = client.DownloadData(url);
        }
    }

    public byte[] Buffer {
        get { return data; }
    }
}

class DataChild : Data, ICloneable {
    public object Clone() {
        return new DataChild(); // Не полная копия.
    }
}

В итоге Вы получите частично скопированный объект. Благодаря тому, что C++ гарантирует наличие конструктора копирования и оператора присваивания (за исключением того, когда явно указывается их отсутствие), любой класс может вызвать родительскую реализацию.

Заключение

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