我正在为一个突破的克隆创建一个简单的SDL2包装器。该项目的真正学习目标是:
了解如何通过包装器正确地管理资源,了解更多关于SDL2的知识,我正在构建这些包装器,因为我正在学习Lazy的教程,并希望获得一些关于我正在做的事情以及我可以做的其他事情的反馈。
到目前为止,这是我的SDL_Surface包装器:
// Surface.h
#pragma once
#include <iostream>
#include <SDL.h>
#include <string>
#include "Window.h"
namespace SDL2 {
class Surface
{
private:
SDL_Surface* ScreenSurface = nullptr;
bool IsWindowSurface = false;
std::string ImagePath = "";
public:
// This function creates a Surface object associated with a Window (gets cleaned when Window is destroyed)
Surface(SDL2::Window& window);
Surface(std::string path);
// Move constructor
Surface(Surface&& source);
// Disable copy constructor
Surface(const Surface& source) = delete;
~Surface();
// Disable copy by assignment
Surface& operator=(const Surface& source) = delete;
// Move assignment
Surface& operator=(Surface&& source);
void FillRect();
void Update(SDL2::Window& window);
bool LoadBMP();
// TODO: Add more parameters at future SDL2 tutorial
void BlitSurface(SDL2::Surface& destination);
};
}
// Surface.cpp
#include "Surface.h"
SDL2::Surface::Surface(SDL2::Window& window) :
ScreenSurface{ SDL_GetWindowSurface(window.GetSDLWindow()) },
IsWindowSurface{ true }
{
}
SDL2::Surface::Surface(std::string path) :
ImagePath{ path },
IsWindowSurface{ false }
{
}
SDL2::Surface::Surface(Surface && source) :
ScreenSurface{ source.ScreenSurface },
IsWindowSurface{ source.IsWindowSurface },
ImagePath{ source.ImagePath }
{
source.ScreenSurface = nullptr;
source.IsWindowSurface = false;
source.ImagePath = "";
}
SDL2::Surface::~Surface()
{
// If surface is not associated with Window, need free the surface with SDL2 call
if (!IsWindowSurface) {
SDL_FreeSurface(ScreenSurface);
}
}
SDL2::Surface & SDL2::Surface::operator=(Surface && source)
{
if (&source == this)
return *this;
// If surface is not associated with Window, need free the surface with SDL2 call
if (!IsWindowSurface) {
SDL_FreeSurface(ScreenSurface);
}
ScreenSurface = source.ScreenSurface;
IsWindowSurface = source.ScreenSurface;
ImagePath = source.ImagePath;
// Return source to stable state
source.ScreenSurface = nullptr;
source.IsWindowSurface = false;
source.ImagePath = "";
return *this;
}
// TODO: Define SDL_Rect parameter and Color parameter (make wrappers for these)
void SDL2::Surface::FillRect()
{
SDL_FillRect(ScreenSurface, NULL, SDL_MapRGB(ScreenSurface->format, 0x00, 0x00, 0xFF));
}
void SDL2::Surface::Update(SDL2::Window& window)
{
SDL_UpdateWindowSurface(window.GetSDLWindow());
}
// This function is called after a non-window surface is created. Check on this return value for success/fail.
bool SDL2::Surface::LoadBMP()
{
// Load image from path
ScreenSurface = SDL_LoadBMP(ImagePath.c_str());
if (!ScreenSurface) {
std::cout << "Unable to load image " << ImagePath << " SDL Error: " << SDL_GetError() << '\n';
return false;
}
return true;
}
// The source Surface is `this`. This function applies the image to the destination surface (though not updated on the screen)
void SDL2::Surface::BlitSurface(SDL2::Surface & destination)
{
// Args: Source, FUTURE, Destination, FUTURE
SDL_BlitSurface(this->ScreenSurface, NULL, destination.ScreenSurface, NULL);
}任何指点都将不胜感激!
发布于 2018-08-20 08:56:42
像这样包装SDL_Surface以在析构函数中实现自动清理是个好主意。但是,当前的实现做出了一些不一定正确的假设。
有很多方法可以抓住一个表面。从Window获取它,从文件中加载,用SDL_CreateSurface手动创建它,或者使用SDL_TTF之类的东西来呈现字体。
Surface类不应该关心曲面是如何创建的。它只需接受一个表面,并知道是否应在销毁时清理该表面,例如:
Surface(SDL_Surface* surface, bool freeOnDestruction);应该替换当前的Window和string构造函数。可以使LoadBMP函数成为接受string并返回Surface的空闲函数。
Surface对Window的依赖似乎是反向的。由于SDL_Window拥有自己的SDL_Surface,因此依赖关系反过来就更有意义了。因此,Window类可以有一个GetSurface()函数,返回一个Surface,而Update()函数作为Window类中的UpdateSurface()更有意义(对于非窗口表面来说,这是没有意义的)。
错误校验
BlitSurface令人困惑,因为您必须记住是调用source.BlitSurface还是dest.BlitSurface。作为成员函数,最好同时使用BlitFrom(source)和BlitTo(dest),它们都调用单个Blit函数。
最好使用免费函数来代替:
void FillSurface(Surface& surface, Color const& color); // fill entire surface!
void FillSurface(Surface& surface, Rect const& rect, Color const& color); // fill a rect on the surface
void BlitSurface(Surface& source, Surface const& dest); // blit whole surfaces
// TODO: rect versions...SDL_BlitSurface有一些条件,您应该在检查之前和之后检查(它不工作在锁定/空表面,它返回一个错误代码失败)。这些应该被处理并输出一条消息/抛出一个错误/终止程序。
在GetSDLSurface()类中有一个Surface函数(类似于GetSDLWindow()函数)是有意义的。在返回表面指针之前,我们可以检查(断言/抛出/打印消息)该函数中的表面指针是否为非空指针。如果Blit和Fill函数是空闲函数,而不是成员,则它们不能直接访问私有成员,必须使用该函数。这意味着当使用曲面时,它们将自动执行非空错误检查。
使用标准库
C++标准库已经包含了一个类来管理对象的生存期,比如:std::unique_ptr。
与检查是否必须释放复制/移动构造函数中的曲面不同,我们可以使用自定义删除器将其包装在unique_ptr中。然后,我们可以使用默认的move构造函数并移动操作符:
#include <iostream>
#include <functional>
#include <memory>
#include <cassert>
// placeholder for testing...
struct SDL_Surface
{
SDL_Surface() { std::cout << "constructor" << std::endl; }
~SDL_Surface() { std::cout << "destructor" << std::endl; }
};
// placeholder for testing...
void SDL_FreeSurface(SDL_Surface* surface)
{
delete surface;
}
class Surface
{
public:
Surface(SDL_Surface* sdlSurface, bool freeOnDestruction);
Surface(Surface&&) = default;
Surface& operator=(Surface&&) = default;
Surface(Surface const&) = delete;
Surface& operator=(Surface const&) = delete;
SDL_Surface* GetSDLSurface() const; // Blit / Fill etc. should use this to get the SDL_Surface.
private:
using SDLSurfacePtrDeleterT = std::function<void(SDL_Surface*)>;
using SDLSurfacePtrT = std::unique_ptr<SDL_Surface, SDLSurfacePtrDeleterT>;
SDLSurfacePtrT m_sdlSurfacePtr;
};
Surface::Surface(SDL_Surface* sdlSurface, bool freeOnDestruction)
{
assert(sdlSurface != nullptr); // no null surfaces!
// (means we don't have to check the inner pointer every time we e.g. blit).
auto deleter = freeOnDestruction ?
SDLSurfacePtrDeleterT([] (SDL_Surface* surface) { SDL_FreeSurface(surface); }) :
SDLSurfacePtrDeleterT([] (SDL_Surface*) { });
m_sdlSurfacePtr = SDLSurfacePtrT(sdlSurface, deleter);
}
SDL_Surface* Surface::GetSDLSurface() const
{
assert(m_sdlSurfacePtr != nullptr); // check this surface hasn't been moved from
// (if the m_sdlSurfacePtr is valid, the inner surface should be valid (or at least non-null) due to the assert in the constructor).
return m_sdlSurfacePtr.get();
}
int main()
{
std::cout << "Construct + Destruct:" << std::endl;
{
auto sdlSurface = new SDL_Surface();
auto surface = Surface(sdlSurface, true);
// should get destructor call...
}
std::cout << "Construct + Leak:" << std::endl;
{
auto sdlSurface = new SDL_Surface();
auto surface = Surface(sdlSurface, false);
// will not get destructor call...
}
std::cout << "Construct + Move + Destruct:" << std::endl;
{
auto sdlSurface = new SDL_Surface();
auto surface = Surface(sdlSurface, true);
auto surface2 = std::move(surface);
// should get destructor call...
}
}发布于 2018-08-20 21:50:21
#pragma once请注意,您在这里放弃了可移植性,因为这是一个常见的非标准编译器扩展。对于几乎所有的应用程序,只要使用支持它的实现,无论是物理上还是逻辑上都不复制文件,而且文件系统不会触发假阳性,那么#杂注一次就可以了。否则,坚持标准,包括警卫,并作出一些努力,以区别警卫的名称。
#include <iostream>
#include <SDL.h>
#include <string>
#include "Window.h"标题不应依赖于首先包含的其他标头。确保这一点的一种方法是在任何其他标头之前包含标头。
可以避免潜在的使用错误,方法是确保组件的.h文件本身解析-没有外部提供的声明或定义.将.h文件作为.c文件的第一行,可以确保.h文件中不缺少组件物理接口所固有的任何关键信息(或者,如果存在,您将在编译.c文件时立即找到该信息)。
也就是说,您的包含应该按照以下顺序进行:
当您向下移动列表时,库更稳定,使用范围更广(因此对其进行了测试)。进一步排序每个子组,例如按路径/名称按字典顺序排列,使维护人员更容易在包含列表变长时快速找到包含。
不要在头文件中使用#include <iostream>。许多C++实现透明地将静态构造函数插入到包含<iostream>的每个转换单元中,即使客户机从未使用<iostream>工具。
bool IsWindowSurface = false;
std::string ImagePath = "";每当您有与其他值的存在相关联的布尔值时,请使用optional类型(助推、Mnmlstc、愚昧、C++17)。
你的表面真的需要知道它是从哪里创建的吗?
Surface(std::string path);复制std::string可能会很昂贵。通过引用const传递此输入参数。
SDL2::Surface::Surface(std::string path) :
ImagePath{ path },
IsWindowSurface{ false } {}如果要将参数视为接收器参数,则将std::move(path)转换为ImagePath。
bool SDL2::Surface::LoadBMP() {
// Load image from path
ScreenSurface = SDL_LoadBMP(ImagePath.c_str());如果表面已经存在,这里会发生什么?如果该曲面是一个窗口表面副本,则泄漏旧表面,ScreenSurface将指向null,因为ImagePath是空的。如果源是以前的BMP,那么旧的表面仍然泄漏和ScreenSurface指向清洁版本的BMP。
与其将每个函数包装到RAII对象中,不如使用std::unique_ptr来管理生存期对象类型。只要您不使用无法优化的析构函数实例来构造std::unique_ptr,那么std::unique_ptr实际上是免费的。
对于所有的资源,您可能已经注意到创建资源并检查句柄以确保其创建的模式。我们可以创建一个通用助手来创建这些资源类型。
template<typename Result, typename Creator, typename... Arguments>
auto make_resource(Creator c, Arguments&&... args)
{
auto r = c(std::forward<Arguments>(args)...);
if (!r) { throw std::system_error(errno, std::generic_category()); }
return Result(r);
}现在,我们需要一种在不增加成本的情况下将std::unique_ptr作为删除器的方法。我们可以将其包装成一个常量值,并在std::unique_ptr请求时返回一个删除函数。
template <typename T, std::decay_t<T> t>
struct constant_t {
constexpr operator T() noexcept const { return t; }
}使用上述两种帮助,我们可以大量生产资源。首先定义指针类型。
using surface_ptr = std::unique_ptr<
SDL_Surface, constant_t<decltype(SDL_FreeSurface), SDL_FreeSurface>>;那么工厂就开始运作了。
inline auto make_surface(const char* filename) {
return make_resource<surface_ptr>(SDL_LoadBMP, filename);
}
inline auto make_surface(SDL_Window* ptr) {
return make_resource<surface_ptr>(SDL_GetWindowSurface, ptr);
}
inline auto make_surface(
std::uint32_t flags, int width, int height, int depth,
std::uint32 Rmask, std::uint32 Gmask, std::uint32_t Bmask, std::uint32_t Amask) {
return make_resource<surface_ptr>(
SDL_CreateRGBSurface, flags, width, height, depth,
Rmask, Gmask, Bmask, Amask);
}然后,您可以在现有的SDL库中使用此surface_ptr。
void meow() {
auto w = make_window("Purr", SDL_WINDOW_POS_UNDEFINED, SDL_WINDOW_POS_UNDEFINED,
640, 480, 0));
auto s = make_surface(w.get());
SDL_FillRect(s.get(), NULL, SDL_MapRGB(s->format, 255, 0, 0));
SDL_UpdateWindowSurface(w.get());
}注:上面的constant_t助手对于C++11是必需的,因为存在一个缺陷,std::integral_constant没有被计算为constexpr表达式。使用C++14,您可以执行以下操作
template <typename T, std::decay_t<T> t>
using constant_t = std::integral_t<T, t>;
using surface_ptr = std::unique_ptr<SDL_Surface, constant_t<decltype(SDL_FreeSurface)*, SDL_FreeSurface>>;使用C++17允许使用auto的非类型模板参数,可以消除冗长的内容。
template <auto t>
using constant_t = std::integral_constant<std::decay_t<decltype(t)>, t>;
using surface_ptr = std::unique_ptr<SDL_Surface, constant_t<SDL_FreeSurface>>;https://codereview.stackexchange.com/questions/202012
复制相似问题