Ce qu'Objective-C faisait en coulisse — getters, setters, protected et la forme canonique C++
En ObjC, @property (nonatomic, strong) générait automatiquement un getter, un setter, et gérait l'ARC. En C++, tu déclares explicitement ce que ton objet sait faire avec lui-même. C'est plus verbeux — et c'est une très bonne chose.
La forme canonique C++ — Rule of 5
En ObjC, le compilateur prenait en charge la copie, l'assignation et le cycle de vie des objets. En C++, tu exprimes ces comportements explicitement. Un objet qui gère des ressources GPU doit définir les cinq opérations suivantes :
/// C++
class Renderer
{
public:
// 1. Constructeur — acquiert les ressources
Renderer(MTL::Device* device,
const std::string& resourcePath,
NS::UInteger width, NS::UInteger height,
MTL::PixelFormat colorPixelFormat,
MTL::PixelFormat depthPixelFormat);
// 2. Destructeur — libère les ressources
~Renderer();
// 3 & 4. Copie — supprimée : un Renderer possède un GPU, on ne le duplique pas
Renderer(const Renderer&) = delete;
Renderer& operator=(const Renderer&) = delete;
// 5 & 6. Move — supprimé aussi : géré par std::unique_ptr dans ApplicationController
Renderer(Renderer&&) = delete;
Renderer& operator=(Renderer&&) = delete;
};
= delete n'est pas "je n'ai pas eu le temps de l'écrire" — c'est une décision explicite de conception. En déclarant la copie supprimée, tu empêches à la compilation toute tentative de dupliquer un objet qui possède des pointeurs GPU raw. std::unique_ptr<Renderer> dans ApplicationController repose précisément sur cette garantie.
La manière propre de l'écrire : créer un fichier que tu inclus quand nécessaire.
/// C++ — NonCopyable.hpp — update:18/05/26
#ifndef NONCOPYABLE_HPP
#define NONCOPYABLE_HPP
class NonCopyable
{
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable(NonCopyable&&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
NonCopyable& operator=(NonCopyable&&) = delete;
};
#endif // NONCOPYABLE_HPP
/// C++ — Renderer.hpp — update:18/05/26
#include "NonCopyable.hpp"
// class Renderer
class Renderer : NonCopyable // Fait le job
L'Objective-C++ pointe vers ta classe, elle ne la copie pas.
Comparaison directe avec Objective-C
// Objective-C — le compilateur génère tout
@interface ApplicationController : NSObject
@property (nonatomic, strong, readonly) MTKView* mtkView; // getter auto
@property (nonatomic, assign) BOOL isPaused; // getter + setter auto
@end
// C++ équivalent — tu écris ce dont tu as besoin
class Renderer
{
public:
// getter — const, ne modifie pas l'objet
MTL::PixelFormat pixelFormat() const { return m_pixelFormat; }
NS::UInteger width() const { return (NS::UInteger)m_viewport.width; }
NS::UInteger height() const { return (NS::UInteger)m_viewport.height; }
bool isPaused() const { return m_paused; }
// setter — valide la donnée avant de l'accepter
void setPaused(bool paused) { m_paused = paused; }
void setPreferredFPS(uint32_t fps)
{
m_preferredFPS = (fps > 0 && fps <= 240) ? fps : 60; // validation intégrée
}
private:
MTL::PixelFormat m_pixelFormat;
MTL::Viewport m_viewport;
bool m_paused = false;
uint32_t m_preferredFPS = 60;
};
Un point rapide sur les équivalences ;
- @property (strong) → private: + constructeur + destructeur
→ en ObjC, strong gérait automatiquement le retain/release via ARC. En C++, tu l'écris une fois dans le constructeur (->retain()) et une fois dans le destructeur (->release()). C'est plus d'écriture, mais tu vois exactement quand la ressource est acquise et libérée. - @property (readonly) → private: membre + public: getter const
→ en ObjC, readonly interdisait le setter depuis l'extérieur. En C++, l'équivalent est de mettre le membre en private et de n'exposer qu'un getter marqué const. - @property (assign) → membre trivial sans gestion mémoire
→ types primitifs (BOOL, NSInteger, CGFloat) en ObjC. Équivalent direct en C++ : bool, int, float — pas de retain, pas de release. - const après la signature
→ float width() const — le const dit au compilateur que cette méthode ne modifie pas l'état de l'objet. Cela permet de l'appeler sur un objet const Renderer& et active des optimisations du compilateur.
L'ARC en Objective-C vs la gestion manuelle en C++
// Ce que tu écrivais en ObjC
@property (nonatomic, strong) MTKView* mtkView;
// Ce que le compilateur générait automatiquement (ARC)
- (MTKView*)mtkView { return _mtkView; }
- (void)setMtkView:(MTKView*)v { [_mtkView release]; _mtkView = [v retain]; }
// Ce que tu écris en C++ dans le constructeur de Renderer
Renderer::Renderer(MTL::Device* device, ...)
: m_device(device->retain()), // retain explicite — tu possèdes le device
m_commandQueue(m_device->newCommandQueue()),
m_shaderLibrary(m_device->newDefaultLibrary())
{}
Renderer::~Renderer()
{
m_shaderLibrary->release(); // release dans l'ordre inverse
m_commandQueue->release();
m_device->release();
}
m_shaderLibrary a été créée depuis m_device, donc elle doit partir avant. Libérer le device en premier alors qu'un objet qui en dépend existe encore est un crash silencieux ou un comportement indéfini.
Visibilité : private / protected / public
/*
* En ObjC, l'encapsulation était optionnelle :
*
* @public → accessible partout (rare, déconseillé)
* @protected → accessible dans la classe et ses sous-classes
* @private → accessible dans la classe seulement (par défaut)
*
* La vraie encapsulation se faisait via les @property et
* les extensions de classe (@interface Foo ()).
*/
// En C++ — déclaré explicitement dans la classe
class Renderer
{
public:
// Accessible depuis n'importe où — interface publique
void draw(MTK::View* view, double timeStamp, float delta);
void resizeViewAndUpdateViewportWindow(NS::UInteger width, NS::UInteger height);
protected:
// Accessible dans Renderer ET ses sous-classes — pas depuis l'extérieur
MTL::Device* m_device;
MTL::CommandQueue* m_commandQueue;
MTL::Library* m_shaderLibrary;
MTL::Viewport m_viewport;
MTL::PixelFormat m_pixelFormat;
private:
// Accessible uniquement dans Renderer — détail d'implémentation
uint64_t m_frame = 0;
uint32_t m_frameIndexBuffer = 0;
bool m_paused = false;
};
protected en pratique — l'héritage de renderer
La vraie utilité de protected apparaît quand tu veux spécialiser le renderer selon la technique de rendu :
/*
* Hiérarchie de renderers
*
* Renderer ← base commune, protected: device, commandQueue
* │
* ┌───────┴───────┐
* ForwardRenderer DeferredRenderer ← accès à protected, pas à private
*/
class ForwardRenderer : public Renderer
{
public:
ForwardRenderer(MTL::Device* device,
const std::string& resourcePath,
NS::UInteger width, NS::UInteger height,
MTL::PixelFormat colorPixelFormat,
MTL::PixelFormat depthPixelFormat);
void draw(MTK::View* view, double timeStamp, float delta) override;
private:
MTL::RenderPipelineState* m_pipelineState; // spécifique au forward
MTL::DepthStencilState* m_depthStencilState;
};
void ForwardRenderer::draw(MTK::View* view, double timeStamp, float delta)
{
// m_device ✓ protected → accessible depuis ForwardRenderer
// m_commandQueue ✓ protected → accessible depuis ForwardRenderer
// m_frame ✗ private → erreur de compilation
auto* commandBuffer = m_commandQueue->commandBuffer();
auto* rpd = view->currentRenderPassDescriptor();
auto* enc = commandBuffer->renderCommandEncoder(rpd);
enc->setViewport(m_viewport); // ✓ protected
enc->setRenderPipelineState(m_pipelineState); // ✓ private de ForwardRenderer
enc->setDepthStencilState(m_depthStencilState);
enc->endEncoding();
commandBuffer->presentDrawable(view->currentDrawable());
commandBuffer->commit();
}
Récapitulatif des trois niveaux ;
- public
→ interface que tu exposes au monde. draw(), resize(), getters. ApplicationController appelle ces méthodes directement via le unique_ptr<Renderer>. - protected
→ ressources GPU partagées entre le renderer de base et ses spécialisations : m_device, m_commandQueue, m_shaderLibrary, m_viewport. Pas de setter nécessaire — les sous-classes les partagent mais ne les remplacent pas. - private
→ état interne pur : compteur de frames, flags de pause, index de triple buffering. Aucune sous-classe n'a de raison légitime d'y toucher directement.
/*
* Tableau de correspondance ObjC → C++
*
* ObjC C++
* ────────────────────────────────────────────────────────
* @property (strong) private: T* m_x; + constructeur/destructeur
* @property (weak) private: T* m_x; (pas de retain/release)
* @property (readonly) private: T m_x; + public: T x() const;
* @property (copy) NSString* private: std::string m_x;
* @public public:
* @protected protected:
* @private (défaut @impl) private: (défaut class)
* alloc/init constructeur
* dealloc destructeur
* ARC retain/release ->retain() / ->release() + Rule of 5
* NSNotification delegate virtual void onEvent() = 0; (interface)
*/
La seule différence entre struct et class en C++ est la visibilité par défaut : struct démarre en public:, class démarre en private:.
Convention dans ce projet : struct pour les données pures sans logique (SceneUniforms, VertexIn...), class pour les objets avec comportement et ressources (Renderer, RMDLZoneWT...).
La POO en C++
La programmation orienté objet en détails
Ce chapitre utilise la classe Camera comme fil conducteur. Elle est représentative d'un code C++ moderne : encapsulation stricte, const-correctness, cache paresseux, transitions animées.
1 — La classe
Une classe regroupe des données (membres) et les fonctions qui les manipulent (méthodes). Elle est le building block de la POO en C++.
/// C++
class Camera
{
public:
Camera();
~Camera();
simd::float3 position() const;
private: // données internes, inaccessibles de l'extérieur
simd::float3 _up;
simd::float3 _position;
simd::float3 _direction;
};
#endif /* Camera_hpp */
2 — Encapsulation : public / private
L'encapsulation protège l'état interne. On expose une interface propre et on cache les détails d'implémentation :
class RMDLCamera
{
public:
// interface publique — stable, documentée
simd::float3 position() const;
simd::float4x4 viewMatrix() const;
void setPosition(simd::float3 newPosition);
private:
// état interne — personne n'y touche directement
simd::float3 _position;
simd::float3 _direction;
bool _uniformsDirty;
RMDLCameraUniforms _uniforms;
// helpers internes — pas dans l'interface
void orthogonalizeFromNewForward(simd::float3 newForward);
void updateOrbitPosition();
};
updateOrbitPosition() est private : c'est un détail d'implémentation.
L'appelant n'a pas à savoir que l'orbit recalcule la position — il appelle juste orbit().
private.
Tout ce qui fait partie du contrat de la classe → public.
En cas de doute, commence par private.
3 — Constructeur & liste d'initialisation
Le constructeur est appelé automatiquement à la création de l'objet.
La liste d'initialisation (après le :) initialise les membres
avant l'entrée dans le corps du constructeur — c'est plus efficace qu'une
assignation dans le corps.
RMDLCamera::RMDLCamera()
: _position{0, 0, 0} // liste d'initialisation
, _direction{0, 0, 1}
, _up{0, 1, 0}
, _viewAngle(0)
, _aspectRatio(1.0)
, _nearPlane(0.1f)
, _farPlane(100.0f)
, _width(0)
, _uniformsDirty(true)
{} // corps vide : tout est fait dans la liste
float, bool, ou des pointeurs bruts, la liste d'initialisation
est obligatoire pour éviter des valeurs indéterminées. Pour les objets (std::string,
std::vector…), ne pas initialiser appelle leur constructeur par défaut — mais
la liste reste plus claire et parfois plus efficace.
Valeurs par défaut des membres (C++11)
On peut aussi initialiser directement dans la déclaration du membre, dans le header. C'est pratique pour les membres qui ont une valeur "naturelle" par défaut :
class RMDLCamera
{
private:
// initialisés directement dans le header
float _yaw = 0.0f;
float _pitch = 0.0f;
float _pitchMin = -M_PI_2 + 0.001f;
float _pitchMax = M_PI_2 - 0.1f;
simd::float3 _orbitTarget = {0, 0, 0};
float _orbitDistance = 10.0f;
bool _orbitMode = false;
bool _transitionActive = false;
};
Si le constructeur fournit aussi une valeur pour ce membre dans sa liste d'initialisation, c'est la valeur du constructeur qui gagne. Les deux mécanismes coexistent sans conflit.
4 — const : méthodes en lecture seule
Une méthode marquée const garantit qu'elle ne modifie pas l'état de l'objet.
Elle peut être appelée sur un objet const ou une référence const&.
// ✅ const : lecture seule, appelable sur const RMDLCamera&
simd::float3 RMDLCamera::position() const { return _position; }
simd::float3 RMDLCamera::forward() const { return _direction; }
float RMDLCamera::nearPlane() const { return _nearPlane; }
bool RMDLCamera::isOrbitMode() const { return _orbitMode; }
// ❌ pas const : modifie _position, _uniformsDirty
void RMDLCamera::setPosition(simd::float3 v)
{
_position = v;
_uniformsDirty = true;
}
const.
Si tu hésites, demande-toi : "est-ce que cet appel peut changer ce que renvoie un autre appel ?" Si non → const.
Const en paramètre
const s'applique aussi aux paramètres. Passer un objet par const&
évite la copie et garantit qu'on ne le modifie pas :
// 'snap' est passé par const référence : pas de copie, pas de modification
void RMDLCamera::applySnapshot(const RMDLCameraSnapshot& snap)
{
_position = snap.position;
_direction = snap.direction;
// ...
_uniformsDirty = true;
}
// 'other' est const : on ne peut appeler que ses méthodes const
void RMDLCamera::transitionTo(const RMDLCamera& other, float duration, /*...*/)
{
transitionTo(other.snapshot(), duration); // snapshot() doit être const
}
5 — mutable : cache dans un objet const
mutable est le seul mot-clé qui permet de modifier un membre depuis une méthode const.
Il sert exclusivement pour les caches : des données dérivées recalculées à la demande,
qui ne font pas partie de l'état logique de l'objet.
Dans RMDLCamera, les matrices view/projection sont dérivées de la position, direction,
fov… Elles ne sont pas l'état — elles en sont la conséquence calculée.
class RMDLCamera
{
private:
// état logique — modifiable seulement depuis les méthodes non-const
simd::float3 _position;
float _viewAngle;
// cache dérivé — modifiable même depuis une méthode const
mutable bool _uniformsDirty;
mutable RMDLCameraUniforms _uniforms;
};
// Peut être const car elle ne touche qu'aux membres mutable
void RMDLCamera::updateUniforms() const
{
_uniforms.viewMatrix = sInvMatrixLookat(_position + _shakeOffset,
_position + _shakeOffset + _direction, _up);
// ... calcul des matrices ...
_uniformsDirty = false; // ✅ OK : _uniformsDirty est mutable
}
RMDLCameraUniforms RMDLCamera::uniforms() const
{
if (_uniformsDirty)
updateUniforms(); // ✅ appel d'une méthode const sur *this const
return _uniforms;
}
mutable n'est pas une excuse pour contourner const. Il est réservé
aux caches et aux membres de synchronisation (mutex). Modifier de l'état logique
depuis une méthode const via mutable est un bug de design.
6 — Le pattern Dirty Flag
Le dirty flag est un pattern de cache paresseux (lazy evaluation) : on marque les données comme obsolètes quand l'état change, et on ne les recalcule qu'au moment où elles sont effectivement nécessaires.
// Chaque setter pose le flag :
void RMDLCamera::setPosition(simd::float3 v)
{
_position = v;
_uniformsDirty = true; // les matrices sont maintenant obsolètes
}
void RMDLCamera::setViewAngle(float v)
{
_width = 0;
_viewAngle = v;
_uniformsDirty = true;
}
// Le getter recalcule seulement si nécessaire :
RMDLCameraUniforms RMDLCamera::uniforms() const
{
if (_uniformsDirty) // recalcul uniquement si quelque chose a changé
updateUniforms();
return _uniforms; // sinon : retour immédiat du cache
}
updateUniforms() calcule 7 matrices (view, proj, viewProj, 3 inverses, frustum planes).
Si tu enchaînes setPosition() + setViewAngle() + setDirection()
dans le même frame, le recalcul n'a lieu qu'une seule fois, au premier appel à uniforms().
Sans dirty flag : 3 recalculs inutiles.
7 — enum class
Un enum class (enum fortement typé, C++11) est préférable au enum classique :
les valeurs sont dans leur propre espace de noms, il n'y a pas de conversion implicite en int,
et les collisions de noms sont impossibles.
// enum class : les valeurs sont préfixées par le type
enum class RMDLCameraEase {
Linear,
SmoothStep,
SmootherStep,
EaseInQuad,
EaseOutQuad,
EaseInOutQuad,
EaseInCubic,
EaseOutCubic,
EaseInOutCubic,
EaseInOutBack,
};
// Utilisation — le compilateur rejette toute confusion de type
camera.transitionTo(snap, 1.5f, RMDLCameraEase::EaseInOutCubic);
// switch exhaustif — le compilateur avertit si un cas manque
float RMDLCamera::applyEase(float t, RMDLCameraEase ease) const
{
switch (ease)
{
case RMDLCameraEase::Linear: return t;
case RMDLCameraEase::SmoothStep: return t * t * (3.0f - 2.0f * t);
case RMDLCameraEase::EaseInQuad: return t * t;
case RMDLCameraEase::EaseOutQuad: return t * (2.0f - t);
// ...
}
return t;
}
8 — std::function comme membre
std::function permet de stocker n'importe quel callable (lambda, fonction, functor)
en tant que membre. Ici il sert de callback de fin de transition — l'appelant passe
ce qu'il veut exécuter quand la caméra arrive à destination.
// Dans le header :
#include <functional>
class RMDLCamera {
private:
std::function<void()> _transitionOnComplete = nullptr;
};
// Signature de transitionTo :
void transitionTo(const RMDLCameraSnapshot& target,
float duration,
RMDLCameraEase ease = RMDLCameraEase::SmoothStep,
std::function<void()> onComplete = nullptr);
// Appelé en fin de transition — usage lambda :
camera.transitionTo(cutsceneShot, 2.0f, RMDLCameraEase::EaseInOutCubic,
[&]() {
startDialogue();
player.setControllable(true);
});
// Dans updateTransition() — appel conditionnel :
if (t >= 1.0f)
{
_transitionActive = false;
applySnapshot(_transitionTo);
if (_transitionOnComplete) // teste si le callback est set
_transitionOnComplete(); // appel
}
9 — struct vs class
En C++, la seule différence entre struct et class est l'accessibilité
par défaut : public pour struct, private pour class.
Par convention, struct est utilisé pour les agrégats de données pures
sans invariants à protéger.
// struct : données publiques, pas de logique métier à protéger
struct RMDLCameraSnapshot
{
simd::float3 position;
simd::float3 direction;
simd::float3 up;
float viewAngle;
float nearPlane;
float farPlane;
simd::float3 orbitTarget;
float orbitDistance;
float yaw;
float pitch;
};
// class : état encapsulé, invariants maintenus, interface publique contrôlée
class RMDLCamera { /* ... */ };
RMDLCameraSnapshot est un simple sac de données. Il peut être copié,
stocké, comparé, interpolé — sans logique. RMDLCamera maintient des invariants
(base orthonormée, dirty flag cohérent, pitch dans ses limites) — d'où la class.
10 — Méthodes inline dans le header
Les petites méthodes (getters d'un mot) peuvent être définies directement dans le header. Le compilateur les inline automatiquement — zéro overhead d'appel de fonction.
class RMDLCamera
{
public:
float yaw() const { return _yaw; }
float pitch() const { return _pitch; }
bool isOrbitMode() const { return _orbitMode; }
simd::float3 orbitTarget() const { return _orbitTarget; }
float orbitDistance() const { return _orbitDistance; }
bool isTransitioning() const { return _transitionActive; }
void cancelTransition() { _transitionActive = false; }
};
.cpp. Exposer le corps dans le header augmente le temps de compilation
et peut forcer des recompilations en cascade si l'implémentation change.
Récapitulatif
RMDLCamera :class / struct → agrégat de données + méthodes.
struct pour les données pures, class pour les objets avec invariants.public / private → encapsuler l'état, exposer une interface stable. Les helpers internes (
updateOrbitPosition) restent private.Liste d'initialisation → initialiser avant le corps du constructeur. Obligatoire pour les membres sans constructeur par défaut.
const méthode → garantit la lecture seule. Permet l'appel sur
const RMDLCamera&.mutable → réservé aux caches. Permet de modifier un membre depuis une méthode
const sans trahir le contrat.Dirty flag → ne recalculer les données dérivées que quand c'est nécessaire. Ici : les 7 matrices GPU.
enum class → type énuméré fortement typé. Pas de conversion implicite, pas de collision de noms.
std::function → stocker un callable générique comme membre. Idéal pour les callbacks de fin d'animation.