Поиск на сайте: Расширенный поиск


Новые программы oszone.net Читать ленту новостей RSS
Файловый менеджер с очень малыми системными требованиями, но тем не менее с большими возможностями. Программа имеет ориг...
Программа RecentX предназначена для упрощения процесса поиска файлов. Она сохраняет информацию обо всех файлах, которые ...
Аудио-конвертер, снабженный удобным «drag and drop»-интерфейсом и поддерживающий большинство распространенных форматов. ...
ChrisTV - неплохая программа для просмотра ТВ на вашем компьютере. Программа работает на картах с чипсетами BT8x8, Phili...
ChrisTV - неплохая программа для просмотра ТВ на вашем компьютере. Программа работает на картах с чипсетами BT8x8, Phili...

Экскурс в C++/CX

Текущий рейтинг: 4.33 (проголосовало 3)
 Посетителей: 1398 | Просмотров: 1721 (сегодня 0)  Шрифт: - +

Готовы к написанию своего первого приложения Windows Store? Или вы уже пишете такие приложения, используя HTML/JavaScript, C# либо Visual Basic, и вас просто интересует вариант с применением C++?

С появлением Visual C++ component extensions (C++/CX) вы можете поднять свою нынешнюю квалификацию на новую высоту, комбинируя код на C++ с богатым набором элементов управления и библиотек, предоставляемых Windows Runtime (WinRT). А если вы используете Direct3D, то можете сделать ваши приложения просто выдающимися в Windows Store.

Когда некоторые слышат о C++/CX, они думают, что им придется освоить совершенно новый язык, но на деле в подавляющем большинстве случаев вы будете иметь дело просто с набором нестандартных языковых элементов, таких как модификатор ^ или ключевые слова ref new. Более того, вы будете использовать эти элементы только на границах своего приложения, т. е. лишь при взаимодействии с Windows Runtime. Ваш портируемый ISO C++ по-прежнему будет выступать в роли рабочей лошадки в этих приложениях. И, возможно, самое главное в том, что код на C++/CX является на 100% неуправляемым. Хотя его синтаксис напоминает C++/Common Language Infrastructure (CLI), ваше приложение не потребует CLR, если только вы сами этого не захотите.

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

В каких случаях выбирать C++/CX?

У каждого приложения собственный набор уникальных требований, равно как и у каждого разработчика свои уникальные навыки и способности. Вы можете успешно создать приложение Windows Store, используя C++, HTML/JavaScript или Microsoft .NET Framework, но вот несколько причин, по которым вы, возможно, выберете C++:

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

Ответ не обязательно должен быть «либо одно, либо другое» — вы можете смешивать языки. Например, когда я писал пример Bing Maps Trip Optimizer (bit.ly/13hkJhA), я использовал HTML и JavaScript для определения UI и C++ для выполнения фоновой обработки. Фоновая обработка в значительной мере решает задачу «задачу коммивояжера». Я применял Parallel Patterns Library (PPL) (bit.ly/155DPtQ) в WinRT-компоненте на C++ для параллельного выполнения алгоритма на всех доступных процессорах, что увеличивает общую производительность. Такое было бы трудно сделать только на JavaScript!

Как работает C++/CX?

Центральное место в любом приложении Windows Store занимает Windows Runtime. А сердцевиной Windows Runtime является двоичный интерфейс приложений (application binary interface, ABI). Библиотеки WinRT определяют метаданные через Windows-файлы метаданных (.winmd). Файл .winmd описывает открытые типы, и его формат напоминает тот, который применяется в сборках .NET Framework. В C++-компоненте файл .winmd содержит только метаданные, а исполняемый код помещается в отдельный файл. Это относится и к WinRT-компонентам, включенным в Windows. (В случае языков .NET Framework файл .winmd содержит и код, и метаданные — точно так же, как сборки .NET Framework.) Вы можете просматривать эти метаданные из MSIL Disassembler (ILDASM) или любое средство чтения CLR-метаданных. На рис. 1 показано, как выглядит в ILDASM файл Windows.Foundation.winmd, содержащий многие фундаментальные WinRT-типы.

*
Рис. 1. Просмотр Windows.Foundation.winmd с помощью ILDASM

ABI опирается на подмножество COM, давая возможность Windows Runtime взаимодействовать со многими языками. Чтобы вызывать WinRT API, .NET Framework и JavaScript требуют проекций, специфичных для каждой языковой среды. Так, нижележащий строковый WinRT-тип HSTRING представляется как System.String в .NET, объект String в JavaScript и ref-класс Platform::String в C++/CX.

Хотя C++ может напрямую взаимодействовать с COM, C++/CX нацелен на упрощение этой задачи за счет:

  • автоматического учета ссылок — WinRT-объекты имеют счетчики ссылок и обычно создаются в куче (не имеет значения, какой язык использует их). Эти объекты уничтожаются, когда их счетчики ссылок обнуляются. Преимущество C++/CX в том, что учет ссылок осуществляется автоматически и единообразно. Это обеспечивается синтаксисом ^;
  • обработки исключений — C++/CX, сообщая о неудаче, полагается на исключения, а не на коды ошибок. Нижележащие COM HRESULT-значения транслируются в WinRT-типы исключений;
  • простого в использовании синтаксиса для работы с WinRT API с сохранением высокой производительности;
  • простого в использовании синтаксиса для создания новых WinRT-типов;
  • простого в использовании синтаксиса для преобразования типов, работы с событиями и других задач.

И помните, что, хотя C++/CX по синтаксису аналогичен C++/CLI, он генерирует чисто неуправляемый код. Вы также можете взаимодействовать с Windows Runtime, используя Windows Runtime C++ Template Library (WRL), о которой я расскажу позже. Надеюсь, что после работы с C++/CX вы согласитесь, что она того стоит. Вы получаете производительность и возможности контроля на уровне неуправляемого кода, вам не надо изучать COM, и ваш код, взаимодействующий с Windows Runtime, будет предельно лаконичным, что позволяет сосредоточиться на базовой логике, делающей ваше приложение уникальным.

Хотя C++/CX по синтаксису аналогичен C++/CLI, он генерирует чисто неуправляемый код.

C++/CX включается параметром компилятора /ZW. Этот ключ задается автоматически, когда вы создаете проект Windows Store в Visual Studio.

Игра в крестики и нолики

Я считаю, что лучший способ изучения нового языка — создать что-то на нем. Чтобы продемонстрировать наиболее важные части C++/CX, я написал приложение Windows Store для игры в крестики и нолики.

Для этого приложения я задействовал шаблон Visual Studio Blank App (XAML). Я назвал проект TicTacToe. В этом проекте для определения UI применяется XAML. Я не буду уделять особого внимания XAML. Чтобы узнать больше об этой стороне дела, прочитайте статью Энди Рича (Andy Rich) «Introducing C++/CX and XAML» (msdn.microsoft.com/magazine/jj651573).

Я также использовал шаблон Windows Runtime Component для создания WinRT-компонента, который определяет логику приложения. Я предпочитаю повторное использование кода, поэтому создал отдельный проект компонента, чтобы любой мог задействовать базовую игровую логику в любом приложении Windows Store, написанном на XAML и C#, Visual Basic или C++.

Как выглядит приложение, показано на рис. 2.

*
Рис. 2. Приложение TicTacToe

Когда я работал над C++-проектом Hilo (bit.ly/Wy5E92), я просто влюбился в шаблон Model-View-ViewModel (MVVM). MVVM — это архитектурный шаблон, помогающий отделить внешний вид (или представление) вашего приложения от нижележащих данных (модели). Модель представления (view model) связывает представление с моделью. Хотя я не использовал все возможности MVVM в своей игре в крестики-нолики, я обнаружил, что применение связывания с данными для отделения UI от логики приложения сделало программу более простой в написании, восприятии и дальнейшем сопровождении. Чтобы узнать больше о том, как мы использовали MVVM в C++-проекте Hilo, см. по ссылке bit.ly/XxigPg.

Чтобы соединить приложение с WinRT-компонентом, я добавил ссылку на проект TicTacToeLibrary из диалога Property Pages проекта TicTacToe.

Простым добавлением ссылки проект TicTacToe получил доступ ко всем открытым C++/CX-типам в проекте TicTacToeLibrary. Указывать какие-либо директивы #include или делать что-то еще не требуется.

Создание TicTacToe UI

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

Определение класса MainPage (MainPage.h) показано на рис. 3.

Рис. 3. Определение класса MainPage

#pragma once
#include "MainPage.g.h"
namespace TicTacToe
{
  public ref class MainPage sealed
  {
  public:
    MainPage();
    property TicTacToeLibrary::GameProcessor^ Processor
    {
      TicTacToeLibrary::GameProcessor^ get() { return m_processor; }
    }
  protected:
    virtual void OnNavigatedTo(     
        Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) override;
  private:
    TicTacToeLibrary::GameProcessor^ m_processor;
  };
}

А что же находится в файле MainPage.g.h? Файл .g.h содержит генерируемое компилятором определение частичного класса для XAML-страниц. В основном эти частичные определения описывают необходимые базовые классы и переменные-члены для любого XAML-элемента, имеющего атрибут x:Name. Вот MainPage.g.h:

namespace TicTacToe
{
  partial ref class MainPage :
    public ::Windows::UI::Xaml::Controls::Page,
    public ::Windows::UI::Xaml::Markup::IComponentConnector
  {
  public:
    void InitializeComponent();
    virtual void Connect(int connectionId, ::Platform::Object^ target);
  private:
    bool _contentLoaded;
  };
}

Ключевое слово partial важно потому, что оно позволяет распределять объявление типа по нескольким файлам. В данном случае MainPage.g.h содержит те части, которые генерируются компилятором, а MainPage.h — дополнительные части, определяемые мной.

Обратите внимание на ключевые слова public и ref в объявлении класса в MainPage. Одно из различий между C++/CX и C++ заключается в концепции доступности класса. Если вы .NET-программист, то знакомы с этой концепцией. Доступность класса подразумевает, будет ли тип или метод виден в метаданных, т. е. доступен внешним компонентам. Тип в C++/CX может быть public или private. Public означает, что к классу MainPage можно обращаться извне модуля (например, из Windows Runtime или другого WinRT-компонента). Тип с ключевым словом private доступен лишь внутри данного модуля. Закрытые типы дают вам больше свободы в использовании C++-типов в открытых методах, что невозможно в случае открытых типов. В нашем случае класс MainPage является открытым (public), поэтому он доступен XAML. Некоторые примеры закрытых типо мы рассмотрим немного позже.

Ключевые слова ref class сообщают компилятору, что это WinRT-тип, а не C++-тип. Такой класс создается в куче, и срок его жизни определяется механизмом учета ссылок. Поскольку ref-типы обеспечивают учет ссылок, их сроки жизни детерминированы. Когда освобождается последняя ссылка на объект ref-типа, вызывается его деструктор и память, выделенная этому объекту, освобождается. Сравните это с .NET, где сроки жизни менее детерминированы и где для освобождения памяти используется сбор мусора.

Создавая экземпляр ref-типа, вы обычно указываете модификатор ^ (произносится как «hat»). Этот модификатор аналогичен C++-указателю (*), но сообщает компилятору вставить код для автоматического управления счетчиком ссылок объекта и удалять объект, когда его счетчик ссылок обнуляется.

Чтобы создать обычную старую структуру (plain old data, POD), используйте значимый класс (value class) или struct. Значимые типы имеют фиксированный размер и состоят только из полей. В отличие от ref-типов они не обладают свойствами. Windows::Foundation::DateTime и Windows::Foundation::Rect — два примера значимых WinRT-типов. При создании экземпляров значимых типов вы не пользуетесь модификатором ^:

Windows::Foundation::Rect bounds(0, 0, 100, 100);

Также заметьте, что MainPage объявлен как запечатанный (sealed). Ключевое слово sealed, аналогичное ключевому слову final в C++11, предотвращает дальнейшее наследование этого типа. MainPage запечатан потому, что у любого открытого ref-типа есть открытый конструктор, который тоже должен объявляться как sealed. Это связано с тем, что исполняющая среда является языково-независимой и не все языки (например, JavaScript) понимают наследование.

Теперь обратите внимание на члены MainPage. Переменная-член m_processor (класс GameProcessor определен в проекте WinRT-компонента — о нем мы поговорим позже) является закрытой просто потому, что класс MainPage запечатан и производному классу нельзя использовать его (и вообще члены данных должны быть по возможности закрытыми для введения инкапсуляции). Метод OnNavigatedTo защищен (protected), поскольку класс Windows::UI::Xaml::Controls::Page, от которого наследует MainPage, объявляет этот метод как protected. Конструктор и свойство Processor должны быть доступными XAML, а значит, оба они объявляются открытыми.

Вы уже знакомы со спецификаторами доступа public, protected и private; их смысл в C++/CX тот же, что в C++. Чтобы узнать об internal и других спецификаторах C++/CX, см. по ссылке bit.ly/Xqb5Xe. Пример internal вы увидите немного позже.

Ref-класс может иметь только общедоступные типы в своих public- и protected-разделах, т. е. лишь элементарные или значимые типы public ref и public. Напротив, C++-тип может содержать ref-типы как переменные-члены, в сигнатурах методов и в локальных переменных функций. Вот пример из проекта Hilo на C++:

std::vector<Windows::Storage::IStorageItem^> m_createdFiles;

Группа программистов Hilo использует std::vector, а не Platform::Collections::Vector для этой закрытой переменной-члена, поскольку данный набор не предоставляется вне класса. Применение std::vector помогает максимально использовать C++-код и делать его намерения четкими.

Перейдем к конструктору MainPage:

MainPage::MainPage() : m_processor(ref
  new TicTacToeLibrary::GameProcessor())
{
  InitializeComponent();
  DataContext = m_processor;
}

Я использую ключевые слова ref new для создания экземпляра объекта GameProcessor. Применение ref new вместо new необходимо для конструирования WinRT-объектов ссылочного типа. При создании объектов в функциях можно использовать ключевое слово auto из C++, чтобы не приходилось столь часто указывать имя типа или ^:

auto processor = ref new TicTacToeLibrary::GameProcessor();

Создание библиотеки TicTacToe

Библиотечный код для игры TicTacToe содержит смесь C++ и C++/CX. В этом приложении я притворился, что у меня был некий код на C++, который я уже написал и протестировал. Я включил этот код напрямую, добавляя код на C++/CX лишь для соединения внутренней реализации с XAML. Иначе говоря, я использовал C++/CX только как мостик между двумя мирами. Давайте пройдемся по некоторым важным частям этой библиотеки и обсудим еще не рассмотренные средства C++/CX.

Класс GameProcessor действует как контекст данных для UI (вспомните о модели данных, если вы знакомы с MVVM). Объявляя этот класс, я указал два атрибута: BindableAttribute и WebHostHiddenAttribute (как и в .NET, при объявлении атрибутов можно опускать часть «Attribute»):

[Windows::UI::Xaml::Data::Bindable]
[Windows::Foundation::Metadata::WebHostHidden]
public ref class GameProcessor sealed : public Common::BindableBase

BindableAttribute создает метаданные, которые сообщают Windows Runtime о том, что тип поддерживает связывание с данными. Это гарантирует, что все открытые свойства типа будут видны XAML-компонентам. Я наследую от BindableBase, чтобы реализовать функциональность, необходимую для работы механизма связывания. Так как BindableBase предназначен для использования в XAML, а не в JavaScript, в нем задействован атрибут WebHostHiddenAttribute (bit.ly/ZsAOV3). По соглашению, я также пометил класс GameProcessor этим атрибутом, чтобы фактически скрыть его от JavaScript.

Я разделил свойства GameProcessor на группы public и internal. Открытые свойства предоставляются XAML, а внутренние — только другим типам и функциям в библиотеке. На мой взгляд, это способствовало большей четкости кода.

Один из распространенных шаблонов использования свойств — связывание наборов с XAML:

property Windows::Foundation::Collections::IObservableVector<Cell^>^ Cells
{
  Windows::Foundation::Collections::IObservableVector<Cell^>^ get()
    { return m_cells; }
}

Это свойство определяет модель данных для ячеек в сетке. Когда значение Cells изменяется, XAML обновляется автоматически. Тип этого свойства — IObservableVector, который является одним из нескольких типов, определенных специально для C++/CX с целью поддержки полного взаимодействия с Windows Runtime. Последняя определяет языково-независимые интерфейсы наборов, а каждый язык реализует эти интерфейсы по-своему. В C++/CX пространство имен Platform::Collections предоставляет такие типы, как Vector и Map, обеспечивающие конкретные реализации этих интерфейсов наборов. Следовательно, я могу объявить свойство Cells как IObservableVector, но поддерживать это свойство объектом Vector, специфичным для C++/CX:

Platform::Collections::Vector<Cell^>^ m_cells;

Итак, когда же отдавать предпочтение Platform::String и наборам Platform::Collections по сравнению со стандартными типами и наборами? Например, что применять для хранения данных — std::vector или Platform::Collections::Vector? В целом, я использую функциональность Platform, когда планирую работать в основном с Windows Runtime, и стандартные типы, такие как std::wstring и std::vector, для своего внутреннего или выполняющего интенсивные вычисления кода. Кроме того, при необходимости вы можете легко осуществлять преобразования между Vector и std::vector. Вы можете создать Vector из std::vector или использовать to_vector для создания std::vector из Vector:

std::vector<int> more_numbers =
  Windows::Foundation::Collections::to_vector(result);

Однако при маршалинге между двумя типами векторов возникают издержки копирования, поэтому тщательно продумайте, какой тип подходит в вашем коде.

Другая распространенная задача — преобразование между std::wstring и Platform::String. Вот как это делается:

// Преобразуем std::wstring в Platform::String
std::wstring s1(L"Hello");
auto s2 = ref new Platform::String(s1.c_str());
// Преобразуем обратно из Platform::String в std::wstring.
// String::Data возвращает строку в стиле C, поэтому вам
// не требуется создавать std::wstring, если вам это не нужно.
std::wstring s3(s2->Data());
// Другой способ обратного преобразования
std::wstring s4(begin(s2), end(s2));

В реализации класса GameProcessor (GameProcessor.cpp) стоит отметить два интересных момента. Во-первых, я использую только стандартный C++ для реализации функции checkEndOfGame. Это одно из мест, где я хочу продемонстрировать, как включить уже написанный и протестированный код на C++.

Второй момент — применение асинхронного программирования. Когда настает время переключения между игроками, я использую PPL-класс task для обработки компьютерных игроков в фоновом режиме, как показано на рис. 4.

Рис. 4. Применение PPL-класса task для фоновой обработки компьютерных игроков

void GameProcessor::SwitchPlayers()
{
// Переключаем игрока переключением указателя
  m_currentPlayer = (m_currentPlayer == m_player1) ? m_player2 : m_player1;
  // Если текущий игрок управляется компьютером, вызываем метод
  // ThinkAsync в фоне, а затем обрабатываем ход компьютера
  if (m_currentPlayer->Player == TicTacToeLibrary::PlayerType::Computer)
  {
    m_currentThinkOp =
      m_currentPlayer->ThinkAsync(ref new Vector<wchar_t>(m_gameBoard));
    m_currentThinkOp->Progress =
      ref new AsyncOperationProgressHandler<uint32, double>([this](
      IAsyncOperationWithProgress<uint32, double>^ asyncInfo, double value)
      {
        (void) asyncInfo; // неиспользуемый параметр
       // Обновляем полоску прогресса
        m_backgroundProgress = value;
        OnPropertyChanged("BackgroundProgress");
      });
      // Создаем задачу, которая обертывает асинхронную
      // операцию. По завершении задачи выделяем ячейку, выбранную компьютером.
      create_task(m_currentThinkOp).then([this](task<uint32> previousTask)
      {
        m_currentThinkOp = nullptr;
        // Я использую здесь продолжение на основе задач, чтобы
        // гарантированно выполнить это продолжение и обновить
        // UI. Вы должны подумать о вставке блока try/catch
        // вокруг вызовов task::get для обработки любых ошибок.
        uint32 move = previousTask.get();
      // Выбираем ячейку
        m_cells->GetAt(move)->Select(nullptr);
       // Сбрасываем полоску прогресса
        m_backgroundProgress = 0.0;
        OnPropertyChanged("BackgroundProgress");
      }, task_continuation_context::use_current());
  }
}

Если вы .NET-программист, рассматривайте task и ее метод then как C++-версию async и await в C#. Задачи доступны из любой программы на C++, но вы будете повсеместно использовать их в своем коде на C++/CX, чтобы ваше приложение Windows Store было быстрым и гибким. Чтобы узнать больше об асинхронном программировании в приложениях Windows, прочитайте статью Артура Лаксберга (Artur Laksberg) «Asynchronous Programming in C++ Using PPL» (msdn.microsoft.com/magazine/hh781020) и статью в MSDN Library по ссылке msdn.microsoft.com/library/hh750082.

Класс Cell моделирует ячейку на игровом поле. Этот класс демонстрирует две новые вещи: события и слабые ссылки (weak references).

Сетка для игровой области TicTacToe состоит из элементов управления Windows::UI::Xaml::Controls::Button. Элемент управления Button генерирует событие Click, но вы также можете реагировать на пользовательский ввод, определив объект ICommand, описывающий контракт передачи команд. Я использую интерфейс ICommand вместо события Click, чтобы объекты Cell могли реагировать напрямую. В XAML для кнопок, определяющих ячейки, свойство Command связывается со свойством Cell::SelectCommand:

<Button Width="133" Height="133" Command="{Binding SelectCommand}"
  Content="{Binding Text}" Foreground="{Binding ForegroundBrush}"
  BorderThickness="2" BorderBrush="White" FontSize="72"/>

Для реализации интерфейса ICommand я задействовал класс DelegateCommand из Hilo. В DelegateCommand есть функция, вызываемая при выдаче команды, и дополнительная (не обязательная) функция, определяющая, может ли быть выдана эта команда. Вот как я настраиваю команду для каждой ячейки:

m_selectCommand = ref new DelegateCommand(
  ref new ExecuteDelegate(this, &Cell::Select), nullptr);

При программировании на XAML вы обычно используете предопределенные события, но можете определять и собственные. Я создал событие, генерируемое при выборе объекта Cell. Класс GameProcessor обрабатывает это событие, проверяя, не закончена ли игра, и при необходимости переключая текущего игрока.

Чтобы создать событие, нужно сначала создать тип делегата. Рассматривайте тип делегата как указатель на функцию или как объект-функцию:

delegate void CellSelectedHandler(Cell^ sender);

Затем я создаю событие для каждого объекта Cell:

event CellSelectedHandler^ CellSelected;

А вот так класс GameProcessor подписывается на это событие для каждой ячейки:

for (auto cell : m_cells)
{
  cell->CellSelected += ref new CellSelectedHandler(
    this, &GameProcessor::CellSelected);
}

Делегат, сконструированный из ^ и функции указателя на член (pointer-to-member function, PMF), хранит только слабую ссылку на объект ^, поэтому такая конструкция не приводит к появлению круговых ссылок.

Вот как объекты Cell генерируют это событие при их выборе:

void Cell::Select(Platform::Object^ parameter)
{
  (void)parameter;
  auto gameProcessor =
    m_gameProcessor.Resolve<GameProcessor>();
  if (m_mark == L'\0' && gameProcessor != nullptr &&
    !gameProcessor->IsThinking &&
    !gameProcessor->CanCreateNewGame)
  {
    m_mark = gameProcessor->CurrentPlayer->Symbol;
    OnPropertyChanged("Text");
    CellSelected(this);
  }
}

Какова цель вызова Resolve в предыдущем коде? Класс GameProcessor хранит набор объектов Cell, но я хочу, чтобы каждый объект Cell мог обращаться к своему родительскому GameProcessor. Если бы Cell содержал жесткую ссылку (strong reference) на своего предка (иначе говоря, GameProcessor^), я создал бы круговую ссылку. Круговые ссылки могут приводить к тому, что объекты никогда не освобождаются, так как взаимная связь заставляет оба объекта всегда поддерживать минимум одну ссылку друг на друга. Чтобы избежать этого, я создаю переменную-член Platform::WeakReference и настраиваю ее из конструктора Cell (продумайте очень тщательно управление сроками жизни и то, чем владеют объекты!):

Platform::WeakReference m_gameProcessor;

При вызове WeakReference::Resolve возвращается nullptr, если объект больше не существует. Поскольку GameProcessor владеет объектами Cell, я ожидаю, что объект GameProcessor всегда будет действителен.

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

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

Работа с интерфейсами

Чтобы различать игрока-человека и игрока, управляемого компьютером, я создал интерфейс IPlayer с конкретными реализациями HumanPlayer и ComputerPlayer. Класс GameProcessor содержит два объекта IPlayer — по одному на каждого игрока — и дополнительную ссылку на текущего игрока:

IPlayer^ m_player1;
IPlayer^ m_player2;
IPlayer^ m_currentPlayer;

Интерфейс IPlayer показан на рис. 5.

Рис. 5. Интерфейс IPlayer

private interface class IPlayer
{
  property PlayerType Player
  {
    PlayerType get();
  }
  property wchar_t Symbol
  {
    wchar_t get();
  }
  virtual Windows::Foundation::IAsyncOperationWithProgress<uint32, double>^
    ThinkAsync(Windows::Foundation::Collections::IVector<wchar_t>^ gameBoard);
};

Поскольку интерфейс IPlayer закрытый, почему же я просто не воспользовался C++-классами? Если честно, я сделал это, чтобы показать, как создать интерфейс и закрытый тип, не публикуемый в метаданных. Если бы я создавал повторно используемую библиотеку, я мог бы объявить IPlayer открытым интерфейсом, чтобы им могли пользоваться другие приложения. В ином случае я мог бы сделать все на C++ и не прибегать к интерфейсу на C++/CX.

Класс ComputerPlayer реализует ThinkAsync, выполняя в фоновом режиме алгоритм minimax (см. файл ComputerPlayer.cpp в сопутствующем коде).

Minimax — распространенный алгоритм при создании компонентов искусственного интеллекта для таких игр, как крестики-нолики. Узнать больше об алгоритме minimax можно в книге Стюарта Рассела (Stuart Russell) и Питера Норвига (Peter Norvig) «Artificial Intelligence: A Modern Approach» (Prentice Hall, 2010).

Я адаптировал алгоритм minimax Рассела и Норвига так, чтобы он выполнялся параллельно, используя PPL (см. minimax.h в сопутствующем коде). Это была отличная возможность задействовать чистый C++11 для написания той части моего приложения, которая интенсивно использует процессор. Мне еще предстоит выиграть у компьютера, и я никогда не видел, чтобы компьютер победил самого себя в игре, где компьютер играет против компьютера. Согласен, что это не самая захватывающая игра, но вы должны добавить дополнительную логику, чтобы человек мог выигрывать у компьютера. Базовый способ сделать это — заставить компьютер делать случайный выбор в случайные моменты времени. Более изощренный способ заключается в том, чтобы компьютер преднамеренно выбирал менее оптимальные варианты в случайные моменты. Добавьте в UI элемент управления «ползунок» (slider control), чтобы настраивать уровень сложности игры (чем ниже уровень сложности, тем чаще компьютер выбирает менее оптимальный ход или, по крайней мере, случайный).

ThinkAsync ничего не делает в случае класса HumanPlayer, поэтому я генерирую исключение Platform::NotImplementedException. По идее, сначала надо проверить свойство IPlayer::Player, но данный вариант избавляет меня от этой задачи:

IAsyncOperationWithProgress<uint32, double>^
  HumanPlayer::ThinkAsync(IVector<wchar_t>^ gameBoard)
{
  (void) gameBoard;
  throw ref new NotImplementedException();
}

WRL

Если C++/CX не способен сделать то, что вам нужно, или чего-то не может, когда вы предпочитаете работать с COM напрямую, то в вашем арсенале есть настоящая кувалда: WRL. Например, создавая медиа расширение для Microsoft Media Foundation, вы должны создать компонент, который реализует как COM-, так и WinRT-интерфейсы. Поскольку ref-классы в C++/CX могут реализовать лишь WinRT-интерфейсы, для создания медиа расширения вы должны использовать WRL — она поддерживает реализацию как COM-, так и WinRT-интерфейсов. Чтобы узнать больше о программировании с применением WRL, см. по ссылке bit.ly/YE8Dxu.

Заключение

Поначалу я испытывал некоторое недоверие к расширениям C++/CX, но вскоре оно рассеялось, и теперь они мне нравятся, потому что позволяют быстро писать приложения Windows Store и использовать идиомы современного C++. Если вы разработчик на C++, настоятельно советую по крайней мере ознакомиться с этими расширениями.

Я рассмотрел лишь некоторые из распространенных шаблонов, с которыми вы встретитесь при написании кода на C++/CX. Проект Hilo — приложение для операций с фотоснимками, использующее C++ и XAML, — гораздо глубже и полнее. Я с удовольствием работал над C++-проектом Hilo и при написании новых приложений часто возвращаюсь к нему. Рекомендую вам посмотреть его по ссылке bit.ly/15xZ5JL.

Автор: Томас Петчел  •  Иcточник: MSDN Magazine  •  Опубликована: 15.08.2013
Нашли ошибку в тексте? Сообщите о ней автору: выделите мышкой и нажмите CTRL + ENTER
Теги:   CX, C++.


Оценить статью:
Вверх
Комментарии посетителей RSS

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