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
46#ifndef CELLPTR_H
47#define CELLPTR_H
48
49#include <wx/debug.h>
50#include <wx/log.h>
51#include <utility>
52#include <cstddef>
53#include <cstdint>
54#include <cinttypes>
55#include <memory>
56#include <type_traits>
57
59#define CELLPTR_CAST_TO_PTR 1
60
62#ifndef CELLPTR_COUNT_INSTANCES
63#define CELLPTR_COUNT_INSTANCES 0
64#endif
65
67#ifndef CELLPTR_LOG_REFS
68#define CELLPTR_LOG_REFS 0
69#endif
70
72#ifndef CELLPTR_LOG_INSTANCES
73#define CELLPTR_LOG_INSTANCES 0
74#endif
75
77#ifndef CELLPTR_LOG_METHOD
78#define CELLPTR_LOG_METHOD wxLogDebug
79#endif
80
81class CellPtrBase;
82
88{
89 class ControlBlock final
90 {
92 Observed *m_object = {};
94 unsigned int m_refCount = 0;
96 static size_t m_instanceCount;
97
98#if CELLPTR_LOG_REFS
99 void LogConstruct(const Observed *) const;
100 void LogRef(const CellPtrBase *) const;
101 void LogDeref(const CellPtrBase *) const;
102 void LogDestruct() const;
103#else
104 static void LogConstruct(const Observed *) {}
105 static void LogRef(const CellPtrBase *) {}
106 static void LogDeref(const CellPtrBase *) {}
107 static void LogDestruct() {}
108#endif
109
110 public:
111 explicit ControlBlock(Observed *object) : m_object(object)
112 {
113#if CELLPTR_COUNT_INSTANCES
114 ++m_instanceCount;
115#endif
116 LogConstruct(object);
117 wxASSERT(object);
118 }
119 ~ControlBlock() {
120#if CELLPTR_COUNT_INSTANCES
121 --m_instanceCount;
122#endif
123 LogDestruct();
124 }
125 ControlBlock(const ControlBlock &) = delete;
126 void operator=(const ControlBlock &) = delete;
127
128 void reset() noexcept { m_object = nullptr; }
129 inline Observed *Get() const noexcept { return m_object; }
130
132 ControlBlock *Ref(const CellPtrBase *cellptr)
133 {
134 LogRef(cellptr);
135 ++m_refCount;
136 return this;
137 }
138
141 bool Deref(const CellPtrBase *cellptr)
142 {
143 LogDeref(cellptr);
144 wxASSERT(m_refCount >= 1);
145 return --m_refCount;
146 }
147
148 static size_t GetLiveInstanceCount() { return m_instanceCount; }
149 };
150
151 static_assert(alignof(ControlBlock) >= 4, "Observed::ControlBlock doesn't have minimum viable alignment");
152
159 class CellPtrImplPointer final
160 {
161 friend void swap(CellPtrImplPointer &a, CellPtrImplPointer &b) noexcept;
162 uintptr_t m_ptr = {};
163 enum Tag : uintptr_t {
164 to_Observed = 0,
165 to_ControlBlock = 1,
166 to_CellPtrBase = 2,
167 to_MASK = 3,
168 to_MASK_OUT = uintptr_t(-intptr_t(to_MASK + 1)),
169 };
170 // TODO: Does this make sense? "| to_Observed" is "| 0". Is this a bug or is
171 // it meant as a readable zero?
172 static uintptr_t ReprFor(Observed *ptr) noexcept
173 { return reinterpret_cast<uintptr_t>(ptr) | to_Observed; }
174 static uintptr_t ReprFor(ControlBlock *ptr) noexcept
175 { return reinterpret_cast<uintptr_t>(ptr) | to_ControlBlock; }
176 static uintptr_t ReprFor(CellPtrBase *ptr) noexcept
177 { return reinterpret_cast<uintptr_t>(ptr) | to_CellPtrBase; }
178 public:
179 constexpr CellPtrImplPointer() noexcept {}
180 constexpr CellPtrImplPointer(const CellPtrImplPointer &) noexcept = default;
181 // cppcheck-suppress noExplicitConstructor
182 constexpr CellPtrImplPointer(decltype(nullptr)) noexcept {}
183 // cppcheck-suppress noExplicitConstructor
184 CellPtrImplPointer(Observed *ptr) noexcept : m_ptr(ReprFor(ptr)) {}
185 // cppcheck-suppress noExplicitConstructor
186 CellPtrImplPointer(ControlBlock *ptr) noexcept : m_ptr(ReprFor(ptr)) {}
187 // cppcheck-suppress noExplicitConstructor
188 CellPtrImplPointer(CellPtrBase *ptr) noexcept : m_ptr(ReprFor(ptr)) {}
189
190 constexpr CellPtrImplPointer &operator=(decltype(nullptr)) noexcept { m_ptr = {}; return *this; }
191 constexpr CellPtrImplPointer &operator=(CellPtrImplPointer o) noexcept { m_ptr = o.m_ptr; return *this; }
192 CellPtrImplPointer &operator=(Observed *ptr) noexcept { m_ptr = ReprFor(ptr); return *this; }
193 CellPtrImplPointer &operator=(ControlBlock *ptr) noexcept { m_ptr = ReprFor(ptr); return *this; }
194 CellPtrImplPointer &operator=(CellPtrBase *ptr) noexcept { m_ptr = ReprFor(ptr); return *this; }
195
196 constexpr explicit inline operator bool() const noexcept { return m_ptr != 0; }
197 constexpr inline bool HasObserved() const noexcept { return (m_ptr & to_MASK) == to_Observed; }
198 constexpr inline bool HasControlBlock() const noexcept { return (m_ptr & to_MASK) == to_ControlBlock; }
199 constexpr inline bool HasCellPtrBase() const noexcept { return (m_ptr & to_MASK) == to_CellPtrBase; }
200 constexpr inline auto GetObserved() const noexcept { static_assert((to_Observed & to_MASK_OUT) == 0, "");
201 return HasObserved() ? reinterpret_cast<Observed *> (m_ptr) : nullptr; }
202 constexpr inline auto GetControlBlock() const noexcept { return HasControlBlock() ? reinterpret_cast<ControlBlock *>(m_ptr & to_MASK_OUT) : nullptr; }
203 constexpr inline auto GetCellPtrBase() const noexcept { return HasCellPtrBase() ? reinterpret_cast<CellPtrBase *> (m_ptr & to_MASK_OUT) : nullptr; }
204
205 constexpr inline auto GetImpl() const noexcept { return m_ptr; }
206 inline auto CastAsObserved() const noexcept { return reinterpret_cast<Observed *>(m_ptr); }
207 inline auto CastAsControlBlock() const noexcept { return reinterpret_cast<ControlBlock *>(m_ptr & to_MASK_OUT); }
208 };
209
210 friend void swap(CellPtrImplPointer &a, CellPtrImplPointer &b) noexcept;
211 friend class CellPtrBase;
212 static size_t m_instanceCount;
213
222 CellPtrImplPointer m_ptr;
223
224 Observed(const Observed &) = delete;
225 void operator=(const Observed &) = delete;
227 void OnEndOfLife() const noexcept;
228
229#if CELLPTR_LOG_REFS
230 void LogRef(const CellPtrBase *) const;
231 void LogDeref(const CellPtrBase *) const;
232#else
233 static void LogRef(const CellPtrBase *) {}
234 static void LogDeref(const CellPtrBase *) {}
235#endif
236
237protected:
238 Observed() noexcept
239#if CELLPTR_COUNT_INSTANCES
240 { ++m_instanceCount; }
241#else
242 = default;
243#endif
244 ~Observed()
245 {
246 if (m_ptr) OnEndOfLife();
247#if CELLPTR_COUNT_INSTANCES
248 --m_instanceCount;
249#endif
250 }
251
252public:
253 static size_t GetLiveInstanceCount() { return m_instanceCount; }
254 static size_t GetLiveControlBlockInstanceCount() { return ControlBlock::GetLiveInstanceCount(); }
255 bool IsNull() const { return !m_ptr.GetImpl(); }
256 bool HasControlBlock() const { return m_ptr.GetControlBlock(); }
257 bool HasOneCellPtr() const { return m_ptr.GetCellPtrBase(); }
258};
259
260inline void swap(Observed::CellPtrImplPointer &a, Observed::CellPtrImplPointer &b) noexcept
261{ std::swap(a.m_ptr, b.m_ptr); }
262
263class Cell;
264class GroupCell;
265
275// cppcheck-suppress ctuOneDefinitionRuleViolation
277{
278 using CellPtrImplPointer = Observed::CellPtrImplPointer;
279 using ControlBlock = Observed::ControlBlock;
280 static size_t m_instanceCount;
281
290 mutable CellPtrImplPointer m_ptr;
291
293 void Ref(Observed *obj);
294
296 void Deref() noexcept;
297
300 decltype(nullptr) DerefControlBlock() const noexcept;
301
302#if CELLPTR_LOG_INSTANCES
303 void LogConstruction(Observed *obj) const;
304 void LogMove(const CellPtrBase &o) const;
305 void LogAssignment(const CellPtrBase &o) const;
306 void LogDestruction() const;
307#else
308 static inline void LogConstruction(Observed *) {}
309 static inline void LogMove(const CellPtrBase &) {}
310 static inline void LogAssignment(const CellPtrBase &) {}
311 static inline void LogDestruction() {}
312#endif
313
314protected:
315 explicit CellPtrBase(Observed *obj = nullptr) noexcept
316 {
317#if CELLPTR_COUNT_INSTANCES
318 ++m_instanceCount;
319#endif
320 if (obj) Ref(obj);
321 LogConstruction(obj);
322 }
323
324 CellPtrBase(const CellPtrBase &o) noexcept : CellPtrBase(o.base_get()) {}
325
326 CellPtrBase(CellPtrBase &&o) noexcept
327 {
328#if CELLPTR_COUNT_INSTANCES
329 ++m_instanceCount;
330#endif
331 LogMove(o);
332 using namespace std;
333
334 auto *thisObserved = m_ptr.GetObserved();
335 if (thisObserved)
336 {
337 wxASSERT(thisObserved->m_ptr.GetCellPtrBase() == this);
338 thisObserved->LogDeref(this);
339 thisObserved->LogRef(&o);
340 thisObserved->m_ptr = &o;
341 }
342
343 auto *otherObserved = o.m_ptr.GetObserved();
344 if (otherObserved)
345 {
346 wxASSERT(otherObserved->m_ptr.GetCellPtrBase() == &o);
347 otherObserved->LogDeref(&o);
348 otherObserved->LogRef(this);
349 otherObserved->m_ptr = this;
350 }
351
352 swap(m_ptr, o.m_ptr);
353 }
354
355 ~CellPtrBase() noexcept
356 {
357#if CELLPTR_COUNT_INSTANCES
358 --m_instanceCount;
359#endif
360 LogDestruction();
361 Deref();
362 }
363
364 CellPtrBase &operator=(const CellPtrBase &o) noexcept
365 {
366 base_reset(o.base_get());
367 return *this;
368 }
369
370 CellPtrBase &operator=(CellPtrBase &&o) noexcept
371 {
372 LogAssignment(o);
373 using namespace std;
374 swap(m_ptr, o.m_ptr);
375 return *this;
376 }
377
378 inline Observed *base_get() const noexcept
379 {
380 // Warning: This function is CRITICAL to the performance of wxMaxima
381 // as a whole!
382 //
383 // The common hot path that iterates cells via the m_nextToDraw uses
384 // this function and is intimately tied to its performance. Small
385 // changes here can cause performance regressions - or small performance
386 // improvements.
387 //
388 // If you change anything, do before- and after- measurements to verify
389 // that whatever improvement you sought is in fact achieved. Changes that
390 // don't measurably improve performance are discouraged.
391
392 // The common path, meant to be hot: if we point directly at the observed object,
393 // just return that. This is also where null is returned if the pointer is null.
394 // `HasObserved()` is a simple bitmask test, and `CastAsObserved` is a binary
395 // NO-OP.
396
397 if (m_ptr.HasObserved())
398 return m_ptr.CastAsObserved();
399
400 // Otherwise, we must be pointing to a control block: get the pointed-to
401 // observed from the control block. Since such use case is meant to be
402 // rare, the overhead of pointer chasing (one extra layer of indirection)
403 // is acceptable.
404
405 auto *const observed = m_ptr.CastAsControlBlock()->Get();
406 if (observed)
407 return observed;
408
409 // We have a control block, but the observed object is gone: we dereference
410 // the zombie control block, to deallocate it as soon as possible, and we
411 // reset ourselves to null. This happens only once per observed object, and
412 // subsequent calls will go via the common path.
413
414 return DerefControlBlock(); // returns null - allows a tail call optimization
415 }
416
417 void base_reset(Observed *obj = nullptr) noexcept;
418
419public:
420 template <typename U>
421 static bool constexpr is_pointer() {
422 return std::is_same<U, decltype(nullptr)>::value
423 || (std::is_pointer<U>::value && std::is_convertible<U, Observed*>::value);
424 }
425
426 explicit operator bool() const noexcept { return base_get(); }
427
428 inline void reset() noexcept { base_reset(); }
429
431 auto cmpPointers(const CellPtrBase &o) const noexcept { return m_ptr.GetImpl() - o.m_ptr.GetImpl(); }
432
434 auto cmpObjects(const CellPtrBase &o) const noexcept { return base_get() - o.base_get(); }
435
437 auto cmpObjects(const Observed *o) const noexcept { return base_get() - o; }
438
439 static size_t GetLiveInstanceCount() noexcept { return m_instanceCount; }
440
441 bool IsNull() const { return !m_ptr.GetImpl(); }
442 bool HasOneObserved() const { return m_ptr.GetObserved(); }
443 bool HasControlBlock() const { return m_ptr.GetControlBlock(); }
444};
445
446static_assert(alignof(Observed) >= 4, "Observed doesn't have minimum viable alignment");
447static_assert(alignof(CellPtrBase) >= 4, "CellPtrBase doesn't have minimum viable alignment");
448
478template <typename T>
479class CellPtr final : public CellPtrBase
480{
481 template <typename U>
482 // cppcheck-suppress duplInheritedMember
483 static bool constexpr is_pointer() {
484 return std::is_same<U, decltype(nullptr)>::value
485 || (std::is_pointer<U>::value && std::is_convertible<U, pointer>::value);
486 }
487public:
488 using value_type = T;
489 using pointer = T*;
490 using const_pointer = const T*;
491 using reference = T&;
492
493 CellPtr() noexcept = default;
494
495 // Observers
496 //
497 pointer get() const noexcept;
498 inline reference operator*() const noexcept { return *get(); }
499 inline pointer operator->() const noexcept { return get(); }
500
501#if CELLPTR_CAST_TO_PTR
502 operator pointer() const noexcept { return get(); }
503#endif
504
505 template <typename PtrT, typename std::enable_if<std::is_pointer<PtrT>::value, bool>::type = true>
506 PtrT CastAs() const noexcept;
507
508 // Operations with NULL and integers in general
509 //
510 explicit CellPtr(int) = delete;
511 explicit CellPtr(void *) = delete;
512
513 // Operations with nullptr_t
514 //
515 // cppcheck-suppress duplInheritedMember
516 void reset() noexcept { base_reset(); }
517 explicit CellPtr(decltype(nullptr)) noexcept {}
518 CellPtr &operator=(decltype(nullptr)) noexcept { base_reset(); return *this; }
519 bool operator==(decltype(nullptr)) const noexcept { return !static_cast<bool>(*this); }
520 bool operator!=(decltype(nullptr)) const noexcept { return static_cast<bool>(*this); }
521
522 // Operations with convertible-to-pointer types
523 //
524 template <typename U, typename std::enable_if<is_pointer<U>(), bool>::type = true>
525 explicit CellPtr(U obj) noexcept : CellPtrBase(obj) {}
526
527 template <typename U, typename std::enable_if<is_pointer<U>(), bool>::type = true>
528 CellPtr &operator=(U obj) noexcept
529 {
530 base_reset(obj);
531 return *this;
532 }
533
534 template <typename U, typename std::enable_if<is_pointer<U>(), bool>::type = true>
535 void reset(U obj) noexcept
536 { base_reset(obj); }
537 // Operations with compatible CellPtrs
538 //
539 CellPtr(CellPtr &o) noexcept : CellPtrBase(o) {}
540 CellPtr(CellPtr &&o) noexcept : CellPtrBase(std::move(o)) {}
541 CellPtr &operator=(const CellPtr &o) noexcept { CellPtrBase::operator=(o); return *this; }
542 CellPtr &operator=(CellPtr &&o) noexcept { CellPtrBase::operator=(std::move(o)); return *this; }
543
544 template <typename U,
545 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
546 // cppcheck-suppress noExplicitConstructor
547 CellPtr(CellPtr<U> &&o) noexcept : CellPtrBase(o) {}
548
549 template <typename U,
550 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
551 // cppcheck-suppress noExplicitConstructor
552 CellPtr(const CellPtr<U> &o) noexcept : CellPtrBase(o.get()) {}
553
554 template <typename U,
555 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
556 CellPtr &operator=(CellPtr<U> &&o) noexcept
557 {
558 CellPtrBase::operator=(o);
559 return *this;
560 }
561 template <typename U,
562 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
563 CellPtr &operator=(const CellPtr<U> &o) noexcept
564 {
565 CellPtrBase::operator=(o);
566 return *this;
567 }
568
569#if !CELLPTR_CAST_TO_PTR
570 template <typename U,
571 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
572 bool operator==(const CellPtr<U> &ptr) const noexcept { return cmpControlBlocks(ptr) == 0; }
573 template <typename U,
574 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
575 bool operator!=(const CellPtr<U> &ptr) const noexcept { return cmpControlBlocks(ptr) != 0; }
576 template <typename U,
577 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
578 bool operator<(const CellPtr<U> &ptr) const noexcept { return cmpObjects(ptr) < 0; }
579#endif
580
581 // Operations with compatible unique_ptr
582 //
583 template <typename U, typename Del>
584 explicit CellPtr(std::unique_ptr<U, Del> &&) = delete;
585
586 template <typename U, typename Del,
587 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
588 explicit CellPtr(const std::unique_ptr<U, Del> &ptr) noexcept : CellPtrBase(ptr.get()) {}
589
590 template <typename U, typename Del>
591 CellPtr &operator=(std::unique_ptr<U, Del> &&) = delete;
592
593 template <typename U, typename Del,
594 typename std::enable_if<std::is_convertible<typename std::add_pointer<U>::type, pointer>::value, bool>::type = true>
595 CellPtr &operator=(const std::unique_ptr<U, Del> &o) noexcept
596 { return *this = o.get(); }
597};
598
599//
600
601template <typename T> typename
602CellPtr<T>::pointer CellPtr<T>::get() const noexcept { return static_cast<pointer>(base_get()); }
603
608template <>
610
611//
612
613template <typename T, typename U>
614bool operator==(const CellPtr<T> &left, const CellPtr<U> &right) noexcept { return left.cmpPointers(right) == 0; }
615
616#if !CELLPTR_CAST_TO_PTR
617template <typename T, typename U,
618 typename std::enable_if<CellPtrBase::is_pointer<U>(), bool>::type = true>
619bool operator==(U left, const CellPtr<T> &right) noexcept { return right.cmpObjects(left) == 0; }
620#endif
621
622template <typename T, typename U,
623 typename std::enable_if<CellPtrBase::is_pointer<U>(), bool>::type = true>
624bool operator==(const CellPtr<T> &left, U right) noexcept { return left.cmpObjects(right) == 0; }
625
626template <typename T, typename U>
627bool operator!=(const CellPtr<T> &left, const CellPtr<U> &right) noexcept { return left.cmpPointers(right) != 0; }
628
629#if !CELLPTR_CAST_TO_PTR
630template <typename T, typename U,
631 typename std::enable_if<CellPtrBase::is_pointer<U>(), bool>::type = true>
632bool operator!=(U left, const CellPtr<T> &right) noexcept { return right.cmpObjects(left) != 0; }
633#endif
634
635template <typename T, typename U,
636 typename std::enable_if<CellPtrBase::is_pointer<U>(), bool>::type = true>
637bool operator!=(const CellPtr<T> &left, U right) noexcept { return left.cmpObjects(right) != 0; }
638
639template <typename T, typename U>
640bool operator<(const CellPtr<T> &left, const CellPtr<U> &right) noexcept { return left.cmpObjects(right) < 0; }
641
642#if !CELLPTR_CAST_TO_PTR
643template <typename T, typename U,
644 typename std::enable_if<CellPtrBase::is_pointer<U>(), bool>::type = true>
645bool operator<(U left, const CellPtr<T> &right) noexcept { return right.cmpObjects(left) > 0; }
646#endif
647
648template <typename T, typename U,
649 typename std::enable_if<CellPtrBase::is_pointer<U>(), bool>::type = true>
650bool operator<(const CellPtr<T> &left, U right) noexcept { return left.cmpObjects(right) < 0; }
651
652//
653
656template<typename Derived, typename Base>
657std::unique_ptr<Derived> static_unique_ptr_cast(std::unique_ptr<Base>&& p) noexcept
658{
659 auto d = static_cast<Derived *>(p.release());
660 return std::unique_ptr<Derived>(d);
661 // Note: We don't move the deleter, since it's not special.
662}
663
666template<typename Derived, typename Base>
667std::unique_ptr<Derived> dynamic_unique_ptr_cast(std::unique_ptr<Base>&& p) noexcept
668{
669 auto d = dynamic_cast<Derived *>(p.get());
670 if (d)
671 (void) p.release();
672 return std::unique_ptr<Derived>(std::move(d));
673 // Note: We don't move the deleter, since it's not special.
674}
675
676#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:657
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:667
An implementation detail for the type-specific templated cell pointers.
Definition: CellPtr.h:277
auto cmpPointers(const CellPtrBase &o) const noexcept
This is exactly like the spaceship operator in C++20.
Definition: CellPtr.h:431
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:434
auto cmpObjects(const Observed *o) const noexcept
This is the spaceship operator acting on pointed-to objects.
Definition: CellPtr.h:437
A weak non-owning pointer that becomes null whenever the observed object is destroyed.
Definition: CellPtr.h:480
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:74
Objects deriving from this class can be observed by the CellPtr.
Definition: CellPtr.h:88
A class that is inherited from the external class Test.
Definition: tag.cpp:5