wxMaxima
Loading...
Searching...
No Matches
CellPtr.h
Go to the documentation of this file.
1// -*- mode: c++; c-file-style: "linux"; c-basic-offset: 2; indent-tabs-mode: nil -*-
2//
3// Copyright (C) 2020 Kuba Ober <kuba@bertec.com>
4//
5// This program is free software; you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation; either version 2 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15//
16// You should have received a copy of the GNU General Public License
17// along with this program; if not, write to the Free Software
18// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19//
20// SPDX-License-Identifier: GPL-2.0+
21
55#ifndef CELLPTR_H
56#define CELLPTR_H
57
58#include <wx/debug.h>
59#include <wx/log.h>
60#include <utility>
61#include <cstddef>
62#include <cstdint>
63#include <cinttypes>
64#include <memory>
65#include <type_traits>
66
68#define CELLPTR_CAST_TO_PTR 1
69
71#ifndef CELLPTR_COUNT_INSTANCES
72#define CELLPTR_COUNT_INSTANCES 0
73#endif
74
76#ifndef CELLPTR_LOG_REFS
77#define CELLPTR_LOG_REFS 0
78#endif
79
81#ifndef CELLPTR_LOG_INSTANCES
82#define CELLPTR_LOG_INSTANCES 0
83#endif
84
86#ifndef CELLPTR_LOG_METHOD
87#define CELLPTR_LOG_METHOD wxLogDebug
88#endif
89
90class CellPtrBase;
91
97{
98 class ControlBlock final
99 {
101 Observed *m_object = {};
103 unsigned int m_refCount = 0;
105 static size_t m_instanceCount;
106
107#if CELLPTR_LOG_REFS
108 void LogConstruct(const Observed *) const;
109 void LogRef(const CellPtrBase *) const;
110 void LogDeref(const CellPtrBase *) const;
111 void LogDestruct() const;
112#else
113 static void LogConstruct(const Observed *) {}
114 static void LogRef(const CellPtrBase *) {}
115 static void LogDeref(const CellPtrBase *) {}
116 static void LogDestruct() {}
117#endif
118
119 public:
120 explicit ControlBlock(Observed *object) : m_object(object)
121 {
122#if CELLPTR_COUNT_INSTANCES
123 ++m_instanceCount;
124#endif
125 LogConstruct(object);
126 wxASSERT(object);
127 }
128 ~ControlBlock() {
129#if CELLPTR_COUNT_INSTANCES
130 --m_instanceCount;
131#endif
132 LogDestruct();
133 }
134 ControlBlock(const ControlBlock &) = delete;
135 void operator=(const ControlBlock &) = delete;
136
137 void reset() noexcept { m_object = nullptr; }
138 inline Observed *Get() const noexcept { return m_object; }
139
141 ControlBlock *Ref(const CellPtrBase *cellptr)
142 {
143 LogRef(cellptr);
144 ++m_refCount;
145 return this;
146 }
147
150 bool Deref(const CellPtrBase *cellptr)
151 {
152 LogDeref(cellptr);
153 wxASSERT(m_refCount >= 1);
154 return --m_refCount;
155 }
156
157 static size_t GetLiveInstanceCount() { return m_instanceCount; }
158 };
159
160 static_assert(alignof(ControlBlock) >= 4, "Observed::ControlBlock doesn't have minimum viable alignment");
161
168 class CellPtrImplPointer final
169 {
170 friend void swap(CellPtrImplPointer &a, CellPtrImplPointer &b) noexcept;
171 uintptr_t m_ptr = {};
172 enum Tag : uintptr_t {
173 to_Observed = 0,
174 to_ControlBlock = 1,
175 to_CellPtrBase = 2,
176 to_MASK = 3,
177 to_MASK_OUT = uintptr_t(-intptr_t(to_MASK + 1)),
178 };
179 // TODO: Does this make sense? "| to_Observed" is "| 0". Is this a bug or is
180 // it meant as a readable zero?
181 static uintptr_t ReprFor(Observed *ptr) noexcept
182 { return reinterpret_cast<uintptr_t>(ptr) | to_Observed; }
183 static uintptr_t ReprFor(ControlBlock *ptr) noexcept
184 { return reinterpret_cast<uintptr_t>(ptr) | to_ControlBlock; }
185 static uintptr_t ReprFor(CellPtrBase *ptr) noexcept
186 { return reinterpret_cast<uintptr_t>(ptr) | to_CellPtrBase; }
187 public:
188 constexpr CellPtrImplPointer() noexcept {}
189 constexpr CellPtrImplPointer(const CellPtrImplPointer &) noexcept = default;
190 // cppcheck-suppress noExplicitConstructor
191 constexpr CellPtrImplPointer(decltype(nullptr)) noexcept {}
192 // cppcheck-suppress noExplicitConstructor
193 CellPtrImplPointer(Observed *ptr) noexcept : m_ptr(ReprFor(ptr)) {}
194 // cppcheck-suppress noExplicitConstructor
195 CellPtrImplPointer(ControlBlock *ptr) noexcept : m_ptr(ReprFor(ptr)) {}
196 // cppcheck-suppress noExplicitConstructor
197 CellPtrImplPointer(CellPtrBase *ptr) noexcept : m_ptr(ReprFor(ptr)) {}
198
199 constexpr CellPtrImplPointer &operator=(decltype(nullptr)) noexcept { m_ptr = {}; return *this; }
200 constexpr CellPtrImplPointer &operator=(CellPtrImplPointer o) noexcept { m_ptr = o.m_ptr; return *this; }
201 CellPtrImplPointer &operator=(Observed *ptr) noexcept { m_ptr = ReprFor(ptr); return *this; }
202 CellPtrImplPointer &operator=(ControlBlock *ptr) noexcept { m_ptr = ReprFor(ptr); return *this; }
203 CellPtrImplPointer &operator=(CellPtrBase *ptr) noexcept { m_ptr = ReprFor(ptr); return *this; }
204
205 constexpr explicit inline operator bool() const noexcept { return m_ptr != 0; }
206 constexpr inline bool HasObserved() const noexcept { return (m_ptr & to_MASK) == to_Observed; }
207 constexpr inline bool HasControlBlock() const noexcept { return (m_ptr & to_MASK) == to_ControlBlock; }
208 constexpr inline bool HasCellPtrBase() const noexcept { return (m_ptr & to_MASK) == to_CellPtrBase; }
209 constexpr inline auto GetObserved() const noexcept { static_assert((to_Observed & to_MASK_OUT) == 0, "");
210 return HasObserved() ? reinterpret_cast<Observed *> (m_ptr) : nullptr; }
211 constexpr inline auto GetControlBlock() const noexcept { return HasControlBlock() ? reinterpret_cast<ControlBlock *>(m_ptr & to_MASK_OUT) : nullptr; }
212 constexpr inline auto GetCellPtrBase() const noexcept { return HasCellPtrBase() ? reinterpret_cast<CellPtrBase *> (m_ptr & to_MASK_OUT) : nullptr; }
213
214 constexpr inline auto GetImpl() const noexcept { return m_ptr; }
215 inline auto CastAsObserved() const noexcept { return reinterpret_cast<Observed *>(m_ptr); }
216 inline auto CastAsControlBlock() const noexcept { return reinterpret_cast<ControlBlock *>(m_ptr & to_MASK_OUT); }
217 };
218
219 friend void swap(CellPtrImplPointer &a, CellPtrImplPointer &b) noexcept;
220 friend class CellPtrBase;
221 static size_t m_instanceCount;
222
231 CellPtrImplPointer m_ptr;
232
233 Observed(const Observed &) = delete;
234 void operator=(const Observed &) = delete;
236 void OnEndOfLife() const noexcept;
237
238#if CELLPTR_LOG_REFS
239 void LogRef(const CellPtrBase *) const;
240 void LogDeref(const CellPtrBase *) const;
241#else
242 static void LogRef(const CellPtrBase *) {}
243 static void LogDeref(const CellPtrBase *) {}
244#endif
245
246protected:
247 Observed() noexcept
248#if CELLPTR_COUNT_INSTANCES
249 { ++m_instanceCount; }
250#else
251 = default;
252#endif
253 ~Observed()
254 {
255 if (m_ptr) OnEndOfLife();
256#if CELLPTR_COUNT_INSTANCES
257 --m_instanceCount;
258#endif
259 }
260
261public:
262 static size_t GetLiveInstanceCount() { return m_instanceCount; }
263 static size_t GetLiveControlBlockInstanceCount() { return ControlBlock::GetLiveInstanceCount(); }
264 bool IsNull() const { return !m_ptr.GetImpl(); }
265 bool HasControlBlock() const { return m_ptr.GetControlBlock(); }
266 bool HasOneCellPtr() const { return m_ptr.GetCellPtrBase(); }
267};
268
269inline void swap(Observed::CellPtrImplPointer &a, Observed::CellPtrImplPointer &b) noexcept
270{ std::swap(a.m_ptr, b.m_ptr); }
271
272class Cell;
273class GroupCell;
274
284// cppcheck-suppress ctuOneDefinitionRuleViolation
286{
287 using CellPtrImplPointer = Observed::CellPtrImplPointer;
288 using ControlBlock = Observed::ControlBlock;
289 static size_t m_instanceCount;
290
299 mutable CellPtrImplPointer m_ptr;
300
302 void Ref(Observed *obj);
303
305 void Deref() noexcept;
306
309 decltype(nullptr) DerefControlBlock() const noexcept;
310
311#if CELLPTR_LOG_INSTANCES
312 void LogConstruction(Observed *obj) const;
313 void LogMove(const CellPtrBase &o) const;
314 void LogAssignment(const CellPtrBase &o) const;
315 void LogDestruction() const;
316#else
317 static inline void LogConstruction(Observed *) {}
318 static inline void LogMove(const CellPtrBase &) {}
319 static inline void LogAssignment(const CellPtrBase &) {}
320 static inline void LogDestruction() {}
321#endif
322
323protected:
324 explicit CellPtrBase(Observed *obj = nullptr) noexcept
325 {
326#if CELLPTR_COUNT_INSTANCES
327 ++m_instanceCount;
328#endif
329 if (obj) Ref(obj);
330 LogConstruction(obj);
331 }
332
333 CellPtrBase(const CellPtrBase &o) noexcept : CellPtrBase(o.base_get()) {}
334
335 CellPtrBase(CellPtrBase &&o) noexcept
336 {
337#if CELLPTR_COUNT_INSTANCES
338 ++m_instanceCount;
339#endif
340 LogMove(o);
341 using namespace std;
342
343 auto *thisObserved = m_ptr.GetObserved();
344 if (thisObserved)
345 {
346 wxASSERT(thisObserved->m_ptr.GetCellPtrBase() == this);
347 thisObserved->LogDeref(this);
348 thisObserved->LogRef(&o);
349 thisObserved->m_ptr = &o;
350 }
351
352 auto *otherObserved = o.m_ptr.GetObserved();
353 if (otherObserved)
354 {
355 wxASSERT(otherObserved->m_ptr.GetCellPtrBase() == &o);
356 otherObserved->LogDeref(&o);
357 otherObserved->LogRef(this);
358 otherObserved->m_ptr = this;
359 }
360
361 swap(m_ptr, o.m_ptr);
362 }
363
364 ~CellPtrBase() noexcept
365 {
366#if CELLPTR_COUNT_INSTANCES
367 --m_instanceCount;
368#endif
369 LogDestruction();
370 Deref();
371 }
372
373 CellPtrBase &operator=(const CellPtrBase &o) noexcept
374 {
375 base_reset(o.base_get());
376 return *this;
377 }
378
379 CellPtrBase &operator=(CellPtrBase &&o) noexcept
380 {
381 LogAssignment(o);
382 using namespace std;
383 swap(m_ptr, o.m_ptr);
384 return *this;
385 }
386
387 inline Observed *base_get() const noexcept
388 {
389 // Warning: This function is CRITICAL to the performance of wxMaxima
390 // as a whole!
391 //
392 // The common hot path that iterates cells via the m_nextToDraw uses
393 // this function and is intimately tied to its performance. Small
394 // changes here can cause performance regressions - or small performance
395 // improvements.
396 //
397 // If you change anything, do before- and after- measurements to verify
398 // that whatever improvement you sought is in fact achieved. Changes that
399 // don't measurably improve performance are discouraged.
400
401 // The common path, meant to be hot: if we point directly at the observed object,
402 // just return that. This is also where null is returned if the pointer is null.
403 // `HasObserved()` is a simple bitmask test, and `CastAsObserved` is a binary
404 // NO-OP.
405
406 if (m_ptr.HasObserved())
407 return m_ptr.CastAsObserved();
408
409 // Otherwise, we must be pointing to a control block: get the pointed-to
410 // observed from the control block. Since such use case is meant to be
411 // rare, the overhead of pointer chasing (one extra layer of indirection)
412 // is acceptable.
413
414 auto *const observed = m_ptr.CastAsControlBlock()->Get();
415 if (observed)
416 return observed;
417
418 // We have a control block, but the observed object is gone: we dereference
419 // the zombie control block, to deallocate it as soon as possible, and we
420 // reset ourselves to null. This happens only once per observed object, and
421 // subsequent calls will go via the common path.
422
423 return DerefControlBlock(); // returns null - allows a tail call optimization
424 }
425
426 void base_reset(Observed *obj = nullptr) noexcept;
427
428public:
429 template <typename U>
430 static bool constexpr is_pointer() {
431 return std::is_same<U, decltype(nullptr)>::value
432 || (std::is_pointer<U>::value && std::is_convertible<U, Observed*>::value);
433 }
434
435 explicit operator bool() const noexcept { return base_get(); }
436
437 inline void reset() noexcept { base_reset(); }
438
440 auto cmpPointers(const CellPtrBase &o) const noexcept { return m_ptr.GetImpl() - o.m_ptr.GetImpl(); }
441
443 auto cmpObjects(const CellPtrBase &o) const noexcept { return base_get() - o.base_get(); }
444
446 auto cmpObjects(const Observed *o) const noexcept { return base_get() - o; }
447
448 static size_t GetLiveInstanceCount() noexcept { return m_instanceCount; }
449
450 bool IsNull() const { return !m_ptr.GetImpl(); }
451 bool HasOneObserved() const { return m_ptr.GetObserved(); }
452 bool HasControlBlock() const { return m_ptr.GetControlBlock(); }
453};
454
455static_assert(alignof(Observed) >= 4, "Observed doesn't have minimum viable alignment");
456static_assert(alignof(CellPtrBase) >= 4, "CellPtrBase doesn't have minimum viable alignment");
457
487template <typename T>
488class CellPtr final : public CellPtrBase
489{
490 template <typename U>
491 // cppcheck-suppress duplInheritedMember
492 static bool constexpr is_pointer() {
493 return std::is_same<U, decltype(nullptr)>::value
494 || (std::is_pointer<U>::value && std::is_convertible<U, pointer>::value);
495 }
496public:
497 using value_type = T;
498 using pointer = T*;
499 using const_pointer = const T*;
500 using reference = T&;
501
502 CellPtr() noexcept = default;
503
504 // Observers
505 //
506 pointer get() const noexcept;
507 inline reference operator*() const noexcept { return *get(); }
508 inline pointer operator->() const noexcept { return get(); }
509
510#if CELLPTR_CAST_TO_PTR
511 operator pointer() const noexcept { return get(); }
512#endif
513
514 template <typename PtrT, typename std::enable_if<std::is_pointer<PtrT>::value, bool>::type = true>
515 PtrT CastAs() const noexcept;
516
517 // Operations with NULL and integers in general
518 //
519 explicit CellPtr(int) = delete;
520 explicit CellPtr(void *) = delete;
521
522 // Operations with nullptr_t
523 //
524 // cppcheck-suppress duplInheritedMember
525 void reset() noexcept { base_reset(); }
526 explicit CellPtr(decltype(nullptr)) noexcept {}
527 CellPtr &operator=(decltype(nullptr)) noexcept { base_reset(); return *this; }
528 bool operator==(decltype(nullptr)) const noexcept { return !static_cast<bool>(*this); }
529 bool operator!=(decltype(nullptr)) const noexcept { return static_cast<bool>(*this); }
530
531 // Operations with convertible-to-pointer types
532 //
533 template <typename U, typename std::enable_if<is_pointer<U>(), bool>::type = true>
534 explicit CellPtr(U obj) noexcept : CellPtrBase(obj) {}
535
536 template <typename U, typename std::enable_if<is_pointer<U>(), bool>::type = true>
537 CellPtr &operator=(U obj) noexcept
538 {
539 base_reset(obj);
540 return *this;
541 }
542
543 template <typename U, typename std::enable_if<is_pointer<U>(), bool>::type = true>
544 void reset(U obj) noexcept
545 { base_reset(obj); }
546 // Operations with compatible CellPtrs
547 //
548 CellPtr(CellPtr &o) noexcept : CellPtrBase(o) {}
549 CellPtr(CellPtr &&o) noexcept : CellPtrBase(std::move(o)) {}
550 CellPtr &operator=(const CellPtr &o) noexcept { CellPtrBase::operator=(o); return *this; }
551 CellPtr &operator=(CellPtr &&o) noexcept { CellPtrBase::operator=(std::move(o)); return *this; }
552
553 template <typename U,
554 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
555 // cppcheck-suppress noExplicitConstructor
556 CellPtr(CellPtr<U> &&o) noexcept : CellPtrBase(o) {}
557
558 template <typename U,
559 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
560 // cppcheck-suppress noExplicitConstructor
561 CellPtr(const CellPtr<U> &o) noexcept : CellPtrBase(o.get()) {}
562
563 template <typename U,
564 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
565 CellPtr &operator=(CellPtr<U> &&o) noexcept
566 {
567 CellPtrBase::operator=(o);
568 return *this;
569 }
570 template <typename U,
571 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
572 CellPtr &operator=(const CellPtr<U> &o) noexcept
573 {
574 CellPtrBase::operator=(o);
575 return *this;
576 }
577
578#if !CELLPTR_CAST_TO_PTR
579 template <typename U,
580 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
581 bool operator==(const CellPtr<U> &ptr) const noexcept { return cmpControlBlocks(ptr) == 0; }
582 template <typename U,
583 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
584 bool operator!=(const CellPtr<U> &ptr) const noexcept { return cmpControlBlocks(ptr) != 0; }
585 template <typename U,
586 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
587 bool operator<(const CellPtr<U> &ptr) const noexcept { return cmpObjects(ptr) < 0; }
588#endif
589
590 // Operations with compatible unique_ptr
591 //
592 template <typename U, typename Del>
593 explicit CellPtr(std::unique_ptr<U, Del> &&) = delete;
594
595 template <typename U, typename Del,
596 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
597 explicit CellPtr(const std::unique_ptr<U, Del> &ptr) noexcept : CellPtrBase(ptr.get()) {}
598
599 template <typename U, typename Del>
600 CellPtr &operator=(std::unique_ptr<U, Del> &&) = delete;
601
602 template <typename U, typename Del,
603 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
604 CellPtr &operator=(const std::unique_ptr<U, Del> &o) noexcept
605 { return *this = o.get(); }
606};
607
608//
609
610template <typename T> typename
611CellPtr<T>::pointer CellPtr<T>::get() const noexcept { return static_cast<pointer>(base_get()); }
612
617template <>
619
620//
621
622template <typename T, typename U>
623bool operator==(const CellPtr<T> &left, const CellPtr<U> &right) noexcept { return left.cmpPointers(right) == 0; }
624
625#if !CELLPTR_CAST_TO_PTR
626template <typename T, typename U,
627 typename std::enable_if<CellPtrBase::is_pointer<U>(), bool>::type = true>
628bool operator==(U left, const CellPtr<T> &right) noexcept { return right.cmpObjects(left) == 0; }
629#endif
630
631template <typename T, typename U,
632 typename std::enable_if<CellPtrBase::is_pointer<U>(), bool>::type = true>
633bool operator==(const CellPtr<T> &left, U right) noexcept { return left.cmpObjects(right) == 0; }
634
635template <typename T, typename U>
636bool operator!=(const CellPtr<T> &left, const CellPtr<U> &right) noexcept { return left.cmpPointers(right) != 0; }
637
638#if !CELLPTR_CAST_TO_PTR
639template <typename T, typename U,
640 typename std::enable_if<CellPtrBase::is_pointer<U>(), bool>::type = true>
641bool operator!=(U left, const CellPtr<T> &right) noexcept { return right.cmpObjects(left) != 0; }
642#endif
643
644template <typename T, typename U,
645 typename std::enable_if<CellPtrBase::is_pointer<U>(), bool>::type = true>
646bool operator!=(const CellPtr<T> &left, U right) noexcept { return left.cmpObjects(right) != 0; }
647
648template <typename T, typename U>
649bool operator<(const CellPtr<T> &left, const CellPtr<U> &right) noexcept { return left.cmpObjects(right) < 0; }
650
651#if !CELLPTR_CAST_TO_PTR
652template <typename T, typename U,
653 typename std::enable_if<CellPtrBase::is_pointer<U>(), bool>::type = true>
654bool operator<(U left, const CellPtr<T> &right) noexcept { return right.cmpObjects(left) > 0; }
655#endif
656
657template <typename T, typename U,
658 typename std::enable_if<CellPtrBase::is_pointer<U>(), bool>::type = true>
659bool operator<(const CellPtr<T> &left, U right) noexcept { return left.cmpObjects(right) < 0; }
660
661//
662
665template<typename Derived, typename Base>
666std::unique_ptr<Derived> static_unique_ptr_cast(std::unique_ptr<Base>&& p) noexcept
667{
668 auto d = static_cast<Derived *>(p.release());
669 return std::unique_ptr<Derived>(d);
670 // Note: We don't move the deleter, since it's not special.
671}
672
675template<typename Derived, typename Base>
676std::unique_ptr<Derived> dynamic_unique_ptr_cast(std::unique_ptr<Base>&& p) noexcept
677{
678 auto d = dynamic_cast<Derived *>(p.get());
679 if (d)
680 (void) p.release();
681 return std::unique_ptr<Derived>(std::move(d));
682 // Note: We don't move the deleter, since it's not special.
683}
684
685#endif // CELLPTR_H
std::unique_ptr< Derived > static_unique_ptr_cast(std::unique_ptr< Base > &&p) noexcept
A cast for unique pointers, used to downcast to a derived type iff we're certain the cell is indeed o...
Definition: CellPtr.h:666
std::unique_ptr< Derived > dynamic_unique_ptr_cast(std::unique_ptr< Base > &&p) noexcept
A cast for unique pointers, used to downcast to a derived type in a type-safe manner.
Definition: CellPtr.h:676
An implementation detail for the type-specific templated cell pointers.
Definition: CellPtr.h:286
auto cmpPointers(const CellPtrBase &o) const noexcept
This is exactly like the spaceship operator in C++20.
Definition: CellPtr.h:440
void base_reset(Observed *obj=nullptr) noexcept
Definition: CellPtr.cpp:155
auto cmpObjects(const CellPtrBase &o) const noexcept
This is the spaceship operator acting on pointed-to objects.
Definition: CellPtr.h:443
auto cmpObjects(const Observed *o) const noexcept
This is the spaceship operator acting on pointed-to objects.
Definition: CellPtr.h:446
A weak non-owning pointer that becomes null whenever the observed object is destroyed.
Definition: CellPtr.h:489
The base class all cell types the worksheet can consist of are derived from.
Definition: Cell.h:142
A cell grouping input (and, if there is one, also the output) cell to a foldable item.
Definition: GroupCell.h:87
Objects deriving from this class can be observed by the CellPtr.
Definition: CellPtr.h:97
A class that is inherited from the external class Test.
Definition: tag.cpp:5