changeset 1742:52705a328b34 by-id

Rejig ById so as to put everything in a single pool, so that at the core you can go from numeric id (untyped) to anything the object can be dynamic_cast to. Useful for building other abstractions like PlayParameter-type registrations that don't know about e.g. Models. Probably some more tweaking needed. Also add tests
author Chris Cannam
date Fri, 28 Jun 2019 17:36:30 +0100
parents 9d82b164f264
children 7001b9570e37
files base/ById.cpp base/ById.h base/PlayParameterRepository.cpp base/PlayParameterRepository.h base/PlayParameters.h base/test/TestById.h base/test/files.pri base/test/svcore-base-test.cpp data/model/DenseTimeValueModel.cpp data/model/EventCommands.h data/model/ImageModel.h data/model/Labeller.h data/model/Model.h data/model/NoteModel.h data/model/RegionModel.h data/model/SparseOneDimensionalModel.h data/model/SparseTimeValueModel.h data/model/TextModel.h data/model/WritableWaveFileModel.cpp files.pri
diffstat 20 files changed, 529 insertions(+), 222 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/ById.cpp	Fri Jun 28 17:36:30 2019 +0100
@@ -0,0 +1,122 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ById.h"
+
+#include <unordered_map>
+#include <typeinfo>
+
+int IdAlloc::getNextId()
+{
+    static int nextId = 0;
+    static QMutex mutex;
+    QMutexLocker locker(&mutex);
+    int i = nextId;
+    if (nextId == INT_MAX) {
+        nextId = INT_MIN;
+    } else {
+        ++nextId;
+        if (nextId == 0 || nextId == NO_ID) {
+            throw std::runtime_error("Internal ID limit exceeded!");
+        }
+    }
+    return i;
+}
+
+class AnyById::Impl
+{
+public:    
+    ~Impl() {
+        QMutexLocker locker(&m_mutex);
+        bool empty = true;
+        for (const auto &p: m_items) {
+            if (p.second && p.second.use_count() > 0) {
+                empty = false;
+                break;
+            }
+        }
+        if (!empty) {
+            SVCERR << "WARNING: ById map is not empty at close; some items have not been released" << endl;
+            SVCERR << "         Unreleased items are:" << endl;
+            for (const auto &p: m_items) {
+                if (p.second && p.second.use_count() > 0) {
+                    SVCERR << "         - id #" << p.first
+                           << ": type " << typeid(*p.second.get()).name()
+                           << ", use count " << p.second.use_count() << endl;
+                }
+            }
+        }
+    }
+        
+    void add(int id, std::shared_ptr<WithId> item) {
+        QMutexLocker locker(&m_mutex);
+        if (m_items.find(id) != m_items.end()) {
+            SVCERR << "ById::add: item with id " << id
+                   << " is already recorded (existing item type is "
+                   << typeid(*m_items.find(id)->second.get()).name()
+                   << ", proposed is "
+                   << typeid(*item.get()).name() << ")" << endl;
+            throw std::logic_error("item id is already recorded in add");
+        }
+        m_items[id] = item;
+    }
+
+    void release(int id) {
+        QMutexLocker locker(&m_mutex);
+        if (m_items.find(id) == m_items.end()) {
+            SVCERR << "ById::release: unknown item id " << id << endl;
+            throw std::logic_error("unknown item id in release");
+        }
+        m_items.erase(id);
+    }
+    
+    std::shared_ptr<WithId> get(int id) const {
+        QMutexLocker locker(&m_mutex);
+        const auto &itr = m_items.find(id);
+        if (itr != m_items.end()) {
+            return itr->second;
+        } else {
+            return {};
+        }
+    }
+
+private:
+    mutable QMutex m_mutex;
+    std::unordered_map<int, std::shared_ptr<WithId>> m_items;
+};
+
+void
+AnyById::add(int id, std::shared_ptr<WithId> item)
+{
+    impl().add(id, item);
+}
+
+void
+AnyById::release(int id)
+{
+    impl().release(id);
+}
+
+std::shared_ptr<WithId>
+AnyById::get(int id)
+{
+    return impl().get(id);
+}
+
+AnyById::Impl &
+AnyById::impl()
+{
+    static Impl impl;
+    return impl;
+}
--- a/base/ById.h	Thu Jun 27 13:08:10 2019 +0100
+++ b/base/ById.h	Fri Jun 28 17:36:30 2019 +0100
@@ -18,145 +18,147 @@
 #include "Debug.h"
 
 #include <memory>
-#include <map>
-#include <typeinfo>
 #include <iostream>
 #include <climits>
+#include <stdexcept>
 
 #include <QMutex>
 #include <QString>
 
 #include "XmlExportable.h"
 
+struct IdAlloc {
+
+    // The value NO_ID (-1) is never allocated
+    static const int NO_ID = -1;
+    
+    static int getNextId();
+};
+
 template <typename T>
-struct SvId {
+struct TypedId {
     
-    int id;
+    int untyped;
+    
+    TypedId() : untyped(IdAlloc::NO_ID) {}
 
-    enum {
-        // The value NO_ID (-1) is never allocated by WithId
-        NO_ID = -1
-    };
-    
-    SvId() : id(NO_ID) {}
+    TypedId(const TypedId &) =default;
+    TypedId &operator=(const TypedId &) =default;
 
-    SvId(const SvId &) =default;
-    SvId &operator=(const SvId &) =default;
-
-    bool operator==(const SvId &other) const { return id == other.id; }
-    bool operator<(const SvId &other) const { return id < other.id; }
-
-    bool isNone() const { return id == NO_ID; }
-
-    QString toString() const {
-        return QString("%1").arg(id);
+    bool operator==(const TypedId &other) const {
+        return untyped == other.untyped;
+    }
+    bool operator!=(const TypedId &other) const {
+        return untyped != other.untyped;
+    }
+    bool operator<(const TypedId &other) const {
+        return untyped < other.untyped;
+    }
+    bool isNone() const {
+        return untyped == IdAlloc::NO_ID;
     }
 };
 
 template <typename T>
 std::ostream &
-operator<<(std::ostream &ostr, const SvId<T> &id)
+operator<<(std::ostream &ostr, const TypedId<T> &id)
 {
     // For diagnostic purposes only. Do not use these IDs for
     // serialisation - see XmlExportable instead.
     if (id.isNone()) {
         return (ostr << "<none>");
     } else {
-        return (ostr << "#" << id.id);
+        return (ostr << "#" << id.untyped);
     }
 }
 
-template <typename T>
 class WithId
 {
 public:
-    typedef SvId<T> Id;
-    
     WithId() :
-        m_id(getNextId()) {
+        m_id(IdAlloc::getNextId()) {
+    }
+    virtual ~WithId() {
     }
 
     /**
-     * Return an id for this object. The id is a unique identifier for
+     * Return an id for this object. The id is a unique number for
      * this object among all objects that implement WithId within this
      * single run of the application.
      */
+    int getUntypedId() const {
+        return m_id;
+    }
+
+private:
+    int m_id;
+};
+
+template <typename T>
+class WithTypedId : virtual public WithId
+{
+public:
+    typedef TypedId<T> Id;
+    
+    WithTypedId() : WithId() { }
+
+    /**
+     * Return an id for this object. The id is a unique value for this
+     * object among all objects that implement WithTypedId within this
+     * single run of the application.
+     */
     Id getId() const {
         Id id;
-        id.id = m_id;
+        id.untyped = getUntypedId();
         return id;
     }
+};
+
+class AnyById
+{
+public:
+    static void add(int, std::shared_ptr<WithId>);
+    static void release(int);
+    static std::shared_ptr<WithId> get(int);
+
+    template <typename Derived>
+    static std::shared_ptr<Derived> getAs(int id) {
+        std::shared_ptr<WithId> p = get(id);
+        return std::dynamic_pointer_cast<Derived>(p);
+    }
 
 private:
-    int m_id;
-
-    static int getNextId() {
-        static int nextId = 0;
-        static QMutex mutex;
-        QMutexLocker locker(&mutex);
-        int i = nextId;
-        if (nextId == INT_MAX) {
-            nextId = INT_MIN;
-        } else {
-            ++nextId;
-            if (nextId == 0 || nextId == Id::NO_ID) {
-                throw std::runtime_error("Internal ID limit exceeded!");
-            }
-        }
-        return i;
-    }
+    class Impl;
+    static Impl &impl();
 };
 
 template <typename Item, typename Id>
-class ById
+class TypedById
 {
 public:
-    ~ById() {
-        QMutexLocker locker(&m_mutex);
-        for (const auto &p: m_items) {
-            if (p.second && p.second.use_count() > 0) {
-                SVCERR << "WARNING: ById map destroyed with use count of "
-                       << p.second.use_count() << " for item with type "
-                       << typeid(*p.second.get()).name()
-                       << " and id " << p.first.id << endl;
-            }
-        }
-    }
-    
-    void add(std::shared_ptr<Item> item) {
-        QMutexLocker locker(&m_mutex);
+    static void add(std::shared_ptr<Item> item) {
         auto id = item->getId();
         if (id.isNone()) {
             throw std::logic_error("item id should never be None");
         }
-        if (m_items.find(id) != m_items.end()) {
-            SVCERR << "WARNING: ById::add: item with id " << id
-                   << " is already recorded, replacing it (item type is "
-                   << typeid(*item.get()).name() << ")" << endl;
-        }
-        m_items[id] = item;
+        AnyById::add(id.untyped, item);
     }
 
-    void
-    release(Id id) {
-        QMutexLocker locker(&m_mutex);
-        m_items.erase(id);
+    static void release(Id id) {
+        AnyById::release(id.untyped);
     }
-
-    std::shared_ptr<Item> get(Id id) const {
-        if (id.isNone()) return {}; // this id is never issued: avoid locking
-        QMutexLocker locker(&m_mutex);
-        const auto &itr = m_items.find(id);
-        if (itr != m_items.end()) {
-            return itr->second;
-        } else {
-            return {};
-        }
+    static void release(std::shared_ptr<Item> item) {
+        release(item->getId());
     }
 
     template <typename Derived>
-    std::shared_ptr<Derived> getAs(Id id) const {
-        return std::dynamic_pointer_cast<Derived>(get(id));
+    static std::shared_ptr<Derived> getAs(Id id) {
+        if (id.isNone()) return {}; // this id is never issued: avoid locking
+        return AnyById::getAs<Derived>(id.untyped);
+    }
+
+    static std::shared_ptr<Item> get(Id id) {
+        return getAs<Item>(id);
     }
 
     /**
@@ -167,7 +169,7 @@
      * The export ID is a simple int, and is only allocated when first
      * requested, so objects that are never exported don't get one.
      */
-    int getExportId(Id id) const {
+    static int getExportId(Id id) {
         auto exportable = getAs<XmlExportable>(id);
         if (exportable) {
             return exportable->getExportId();
@@ -175,44 +177,6 @@
             return XmlExportable::NO_ID;
         }
     }
-    
-private:
-    mutable QMutex m_mutex;
-    std::map<Id, std::shared_ptr<Item>> m_items;
-};
-
-template <typename Item, typename Id>
-class StaticById
-{
-public:
-    static void add(std::shared_ptr<Item> imagined) {
-        byId().add(imagined);
-    }
-
-    static void release(Id id) {
-        byId().release(id);
-    }
-
-    static std::shared_ptr<Item> get(Id id) {
-        return byId().get(id);
-    }
-
-    template <typename Derived>
-    static
-    std::shared_ptr<Derived> getAs(Id id) {
-        return std::dynamic_pointer_cast<Derived>(get(id));
-    }
-
-    static int getExportId(Id id) {
-        return byId().getExportId(id);
-    }
-    
-private:
-    static
-    ById<Item, Id> &byId() {
-        static ById<Item, Id> b;
-        return b;
-    }
 };
 
 #endif
--- a/base/PlayParameterRepository.cpp	Thu Jun 27 13:08:10 2019 +0100
+++ b/base/PlayParameterRepository.cpp	Fri Jun 28 17:36:30 2019 +0100
@@ -17,6 +17,8 @@
 #include "PlayParameters.h"
 #include "Playable.h"
 
+#include "ById.h"
+
 #include <iostream>
 
 PlayParameterRepository *
@@ -33,19 +35,25 @@
 }
 
 void
-PlayParameterRepository::addPlayable(const Playable *playable)
+PlayParameterRepository::addPlayable(int playableId)
 {
 //    cerr << "PlayParameterRepository:addPlayable playable = " << playable <<  endl;
 
-    if (!getPlayParameters(playable)) {
+    if (!getPlayParameters(playableId)) {
 
+        auto playable = AnyById::getAs<Playable>(playableId);
+        if (!playable) {
+            SVCERR << "ERROR: id passed to PlayParameterRepository::addPlayable is not that of a Playable" << endl;
+            return;
+        }
+        
         // Give all playables the same type of play parameters for the
         // moment
 
 //        cerr << "PlayParameterRepository:addPlayable: Adding play parameters for " << playable << endl;
 
         PlayParameters *params = new PlayParameters;
-        m_playParameters[playable] = params;
+        m_playParameters[playableId] = params;
 
         params->setPlayClipId
             (playable->getDefaultPlayClipId());
@@ -65,18 +73,18 @@
 }    
 
 void
-PlayParameterRepository::removePlayable(const Playable *playable)
+PlayParameterRepository::removePlayable(int playableId)
 {
-    if (m_playParameters.find(playable) == m_playParameters.end()) {
-        cerr << "WARNING: PlayParameterRepository::removePlayable: unknown playable " << playable << endl;
+    if (m_playParameters.find(playableId) == m_playParameters.end()) {
+        SVCERR << "WARNING: PlayParameterRepository::removePlayable: unknown playable " << playableId << endl;
         return;
     }
-    delete m_playParameters[playable];
-    m_playParameters.erase(playable);
+    delete m_playParameters[playableId];
+    m_playParameters.erase(playableId);
 }
 
 void
-PlayParameterRepository::copyParameters(const Playable *from, const Playable *to)
+PlayParameterRepository::copyParameters(int from, int to)
 {
     if (!getPlayParameters(from)) {
         cerr << "ERROR: PlayParameterRepository::copyParameters: source playable unknown" << endl;
@@ -90,10 +98,12 @@
 }
 
 PlayParameters *
-PlayParameterRepository::getPlayParameters(const Playable *playable) 
+PlayParameterRepository::getPlayParameters(int playableId) 
 {
-    if (m_playParameters.find(playable) == m_playParameters.end()) return nullptr;
-    return m_playParameters.find(playable)->second;
+    if (m_playParameters.find(playableId) == m_playParameters.end()) {
+        return nullptr;
+    }
+    return m_playParameters.find(playableId)->second;
 }
 
 void
--- a/base/PlayParameterRepository.h	Thu Jun 27 13:08:10 2019 +0100
+++ b/base/PlayParameterRepository.h	Fri Jun 28 17:36:30 2019 +0100
@@ -35,11 +35,37 @@
 
     virtual ~PlayParameterRepository();
 
-    void addPlayable(const Playable *playable);
-    void removePlayable(const Playable *playable);
-    void copyParameters(const Playable *from, const Playable *to);
+    /**
+     * Register a playable.
+     * 
+     * The id must be of an object that is registered with the ById
+     * store and that can be dynamic_cast to Playable.
+     */
+    void addPlayable(int playableId);
 
-    PlayParameters *getPlayParameters(const Playable *playable);
+    /**
+     * Unregister a playable.
+     * 
+     * The id must be of an object that is registered with the ById
+     * store and that can be dynamic_cast to Playable.
+     */
+    void removePlayable(int playableId);
+
+    /**
+     * Copy the play parameters from one playable to another.
+     * 
+     * The ids must be of objects that are registered with the ById
+     * store and that can be dynamic_cast to Playable.
+     */
+    void copyParameters(int fromId, int toId);
+
+    /**
+     * Retrieve the play parameters for a playable.
+     * 
+     * The id must be of an object that is registered with the ById
+     * store and that can be dynamic_cast to Playable.
+     */
+    PlayParameters *getPlayParameters(int playableId);
 
     void clear();
 
@@ -64,14 +90,14 @@
 
 signals:
     void playParametersChanged(PlayParameters *);
-    void playClipIdChanged(const Playable *, QString);
+    void playClipIdChanged(int playableId, QString);
 
 protected slots:
     void playParametersChanged();
     void playClipIdChanged(QString);
 
 protected:
-    typedef std::map<const Playable *, PlayParameters *> PlayableParameterMap;
+    typedef std::map<int, PlayParameters *> PlayableParameterMap;
     PlayableParameterMap m_playParameters;
 
     static PlayParameterRepository *m_instance;
--- a/base/PlayParameters.h	Thu Jun 27 13:08:10 2019 +0100
+++ b/base/PlayParameters.h	Fri Jun 28 17:36:30 2019 +0100
@@ -37,8 +37,8 @@
     virtual void copyFrom(const PlayParameters *);
 
     void toXml(QTextStream &stream,
-                       QString indent = "",
-                       QString extraAttributes = "") const override;
+               QString indent = "",
+               QString extraAttributes = "") const override;
 
 public slots:
     virtual void setPlayMuted(bool muted);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/test/TestById.h	Fri Jun 28 17:36:30 2019 +0100
@@ -0,0 +1,191 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "../ById.h"
+
+#include <QObject>
+#include <QtTest>
+
+#include <iostream>
+
+using namespace std;
+
+struct WithoutId {};
+
+struct A : public WithTypedId<A> {};
+struct B1 : public A {};
+struct B2 : public A {};
+
+struct M {};
+
+typedef TypedById<A, A::Id> AById;
+
+struct X : virtual public WithId {};
+struct Y : public X, public B2, public M {};
+
+class TestById : public QObject
+{
+    Q_OBJECT
+
+private slots:
+    void ids() {
+        // Verify that ids are unique across all classes, not just
+        // within a class. These must be the first two WithId objects
+        // allocated in the first test in the suite, otherwise they
+        // could be different even if they were allocated from
+        // separate pools.
+        A a;
+        X x;
+        if (a.getId().untyped == x.getUntypedId()) {
+            cerr << "ERROR: a and x have the same id: " << a.getId() << endl;
+        }
+        QVERIFY(a.getId().untyped != x.getUntypedId());
+
+        A aa;
+        QVERIFY(aa.getId().untyped != a.getId().untyped);
+        QVERIFY(aa.getId().untyped != x.getUntypedId());
+
+        // Check the actual ids that have been allocated. This is
+        // supposed to be a hidden implementation detail, but we want
+        // to make sure the test itself hasn't become broken in terms
+        // of allocation order (see comment above)
+        QCOMPARE(a.getId().untyped, 0);
+        QCOMPARE(x.getUntypedId(), 1);
+        QCOMPARE(aa.getId().untyped, 2);
+
+        QVERIFY(!a.getId().isNone());
+        QVERIFY(A::Id().isNone());
+    }
+
+    // NB each test must release all the items it adds to the ById store
+    
+    void anyEmpty() {
+        auto p = AnyById::get(0);
+        QVERIFY(!p);
+    }
+
+    void anySimple() {
+        auto a = std::make_shared<A>();
+        AnyById::add(a->getId().untyped, a);
+
+        auto aa = AnyById::getAs<A>(a->getId().untyped);
+        QVERIFY(!!aa);
+        QCOMPARE(aa->getId(), a->getId());
+        QCOMPARE(aa.get(), a.get()); // same object, not just same id!
+        AnyById::release(a->getId().untyped);
+    }
+    
+    void typedEmpty() {
+        auto p = AById::get({});
+        QVERIFY(!p);
+    }
+
+    void typedSimple() {
+        auto a = std::make_shared<A>();
+        AById::add(a);
+
+        auto aa = AById::get(a->getId());
+        QVERIFY(!!aa);
+        QCOMPARE(aa->getId(), a->getId());
+        QCOMPARE(aa.get(), a.get()); // same object, not just same id!
+        AById::release(a);
+    }
+
+    void typedRelease() {
+        auto a = std::make_shared<A>();
+        AById::add(a);
+
+        auto aa = AById::get(a->getId());
+        QVERIFY(!!aa);
+        AById::release(a);
+
+        aa = AById::get(a->getId());
+        QVERIFY(!aa);
+    }
+
+    void typedDowncast() {
+        auto a = std::make_shared<A>();
+        auto b1 = std::make_shared<B1>();
+        AById::add(a);
+        AById::add(b1);
+
+        auto bb1 = AById::getAs<B1>(a->getId());
+        QVERIFY(!bb1);
+
+        bb1 = AById::getAs<B1>(b1->getId());
+        QVERIFY(!!bb1);
+        QCOMPARE(bb1->getId(), b1->getId());
+
+        auto bb2 = AById::getAs<B2>(b1->getId());
+        QVERIFY(!bb2);
+
+        AById::release(a);
+        AById::release(b1);
+    }
+
+    void typedCrosscast() {
+        auto y = std::make_shared<Y>();
+        AById::add(y);
+
+        auto yy = AById::getAs<Y>(y->getId());
+        QVERIFY(!!yy);
+        QCOMPARE(yy->getId(), y->getId());
+        
+        yy = AnyById::getAs<Y>(y->getId().untyped);
+        QVERIFY(!!yy);
+        QCOMPARE(yy->getId(), y->getId());
+
+        auto xx = AById::getAs<X>(y->getId());
+        QVERIFY(!!xx);
+        QCOMPARE(xx->getUntypedId(), y->getId().untyped);
+        QCOMPARE(xx.get(), yy.get());
+        
+        xx = AnyById::getAs<X>(y->getId().untyped);
+        QVERIFY(!!xx);
+        QCOMPARE(xx->getUntypedId(), y->getId().untyped);
+        QCOMPARE(xx.get(), yy.get());
+        
+        auto mm = AnyById::getAs<M>(y->getId().untyped);
+        QVERIFY(!!mm);
+        QCOMPARE(mm.get(), yy.get());
+
+        AById::release(y);
+    }
+
+    void duplicateAdd() {
+        auto a = std::make_shared<A>();
+        AById::add(a);
+        try {
+            AById::add(a);
+            cerr << "Failed to catch expected exception in duplicateAdd" << endl;
+            QVERIFY(false);
+        } catch (const std::logic_error &) {
+        }
+        AById::release(a);
+    }
+
+    void unknownRelease() {
+        auto a = std::make_shared<A>();
+        auto b1 = std::make_shared<B1>();
+        AById::add(a);
+        try {
+            AById::release(b1);
+            cerr << "Failed to catch expected exception in unknownRelease" << endl;
+            QVERIFY(false);
+        } catch (const std::logic_error &) {
+        }
+        AById::release(a);
+    }
+};
+
--- a/base/test/files.pri	Thu Jun 27 13:08:10 2019 +0100
+++ b/base/test/files.pri	Fri Jun 28 17:36:30 2019 +0100
@@ -1,4 +1,5 @@
 TEST_HEADERS = \
+	     TestById.h \
 	     TestColumnOp.h \
 	     TestLogRange.h \
 	     TestMovingMedian.h \
--- a/base/test/svcore-base-test.cpp	Thu Jun 27 13:08:10 2019 +0100
+++ b/base/test/svcore-base-test.cpp	Fri Jun 28 17:36:30 2019 +0100
@@ -20,6 +20,7 @@
 #include "TestVampRealTime.h"
 #include "TestColumnOp.h"
 #include "TestMovingMedian.h"
+#include "TestById.h"
 #include "TestEventSeries.h"
 #include "StressEventSeries.h"
 
@@ -91,13 +92,19 @@
         if (QTest::qExec(&t, argc, argv) == 0) ++good;
         else ++bad;
     }
-/*
+    {
+        TestById t;
+        if (QTest::qExec(&t, argc, argv) == 0) ++good;
+        else ++bad;
+    }
+
+#ifdef NOT_DEFINED
     {
         StressEventSeries t;
         if (QTest::qExec(&t, argc, argv) == 0) ++good;
         else ++bad;
     }
-*/
+#endif
     
     if (bad > 0) {
         SVCERR << "\n********* " << bad << " test suite(s) failed!\n" << endl;
--- a/data/model/DenseTimeValueModel.cpp	Thu Jun 27 13:08:10 2019 +0100
+++ b/data/model/DenseTimeValueModel.cpp	Fri Jun 28 17:36:30 2019 +0100
@@ -20,12 +20,12 @@
 
 DenseTimeValueModel::DenseTimeValueModel()
 {
-    PlayParameterRepository::getInstance()->addPlayable(this);
+    PlayParameterRepository::getInstance()->addPlayable(getId().untyped);
 }
 
 DenseTimeValueModel::~DenseTimeValueModel()
 {
-    PlayParameterRepository::getInstance()->removePlayable(this);
+    PlayParameterRepository::getInstance()->removePlayable(getId().untyped);
 }
         
 QString
--- a/data/model/EventCommands.h	Thu Jun 27 13:08:10 2019 +0100
+++ b/data/model/EventCommands.h	Fri Jun 28 17:36:30 2019 +0100
@@ -30,38 +30,34 @@
     virtual void remove(Event e) = 0;
 };
 
-template <typename Base>
 class WithEditable
 {
-    typedef typename Base::Id Id;
-    Id m_id;
-    
 protected:
-    WithEditable(Id id) : m_id(id) { }
+    WithEditable(int editableId) : m_editableId(editableId) { }
 
     std::shared_ptr<EventEditable> getEditable() {
-        auto base = StaticById<Base, Id>::get(m_id);
-        if (!base) return {}; // acceptable - item has expired
-        auto editable = std::dynamic_pointer_cast<EventEditable>(base);
+        auto editable = AnyById::getAs<EventEditable>(m_editableId);
         if (!editable) {
             SVCERR << "WARNING: Id passed to EventEditable command is not that of an EventEditable" << endl;
         }
         return editable;
     }
+
+private:
+    int m_editableId;
 };
 
 /**
  * Command to add an event to an editable containing events, with
- * undo.  The template parameter must be a type that can be
- * dynamic_cast to EventEditable and that has a ById store.
+ * undo.  The id must be that of a type that can be retrieved from the
+ * AnyById store and dynamic_cast to EventEditable.
  */
-template <typename Base>
 class AddEventCommand : public Command,
-                        public WithEditable<Base>
+                        public WithEditable
 {
 public:
-    AddEventCommand(typename Base::Id editable, const Event &e, QString name) :
-        WithEditable<Base>(editable), m_event(e), m_name(name) { }
+    AddEventCommand(int editableId, const Event &e, QString name) :
+        WithEditable(editableId), m_event(e), m_name(name) { }
 
     QString getName() const override { return m_name; }
     Event getEvent() const { return m_event; }
@@ -78,21 +74,19 @@
 private:
     Event m_event;
     QString m_name;
-    using WithEditable<Base>::getEditable;
 };
 
 /**
  * Command to remove an event from an editable containing events, with
- * undo.  The template parameter must be a type that implements
- * EventBase and that has a ById store.
+ * undo.  The id must be that of a type that can be retrieved from the
+ * AnyById store and dynamic_cast to EventEditable.
  */
-template <typename Base>
 class RemoveEventCommand : public Command,
-                           public WithEditable<Base>
+                           public WithEditable
 {
 public:
-    RemoveEventCommand(typename Base::Id editable, const Event &e, QString name) :
-        WithEditable<Base>(editable), m_event(e), m_name(name) { }
+    RemoveEventCommand(int editableId, const Event &e, QString name) :
+        WithEditable(editableId), m_event(e), m_name(name) { }
 
     QString getName() const override { return m_name; }
     Event getEvent() const { return m_event; }
@@ -109,32 +103,26 @@
 private:
     Event m_event;
     QString m_name;
-    using WithEditable<Base>::getEditable;
 };
 
 /**
  * Command to add or remove a series of events to or from an editable,
  * with undo. Creates and immediately executes a sub-command for each
  * add/remove requested. Consecutive add/remove pairs for the same
- * point are collapsed.  The template parameter must be a type that
- * implements EventBase and that has a ById store.
+ * point are collapsed.  The id must be that of a type that can be
+ * retrieved from the AnyById store and dynamic_cast to EventEditable.
  */
-template <typename Base>
 class ChangeEventsCommand : public MacroCommand
 {
-    typedef typename Base::Id Id;
-    
 public:
-    ChangeEventsCommand(Id editable, QString name) :
-        MacroCommand(name), m_editable(editable) { }
+    ChangeEventsCommand(int editableId, QString name) :
+        MacroCommand(name), m_editableId(editableId) { }
 
     void add(Event e) {
-        addCommand(new AddEventCommand<Base>(m_editable, e, getName()),
-                   true);
+        addCommand(new AddEventCommand(m_editableId, e, getName()), true);
     }
     void remove(Event e) {
-        addCommand(new RemoveEventCommand<Base>(m_editable, e, getName()),
-                   true);
+        addCommand(new RemoveEventCommand(m_editableId, e, getName()), true);
     }
 
     /**
@@ -166,10 +154,10 @@
             return;
         }
         
-        RemoveEventCommand<Base> *r =
-            dynamic_cast<RemoveEventCommand<Base> *>(command);
-        AddEventCommand<Base> *a =
-            dynamic_cast<AddEventCommand<Base> *>(*m_commands.rbegin());
+        RemoveEventCommand *r =
+            dynamic_cast<RemoveEventCommand *>(command);
+        AddEventCommand *a =
+            dynamic_cast<AddEventCommand *>(*m_commands.rbegin());
         if (r && a) {
             if (a->getEvent() == r->getEvent()) {
                 deleteCommand(a);
@@ -180,7 +168,7 @@
         MacroCommand::addCommand(command);
     }
 
-    Id m_editable;
+    int m_editableId;
 };
 
 #endif
--- a/data/model/ImageModel.h	Thu Jun 27 13:08:10 2019 +0100
+++ b/data/model/ImageModel.h	Fri Jun 28 17:36:30 2019 +0100
@@ -231,7 +231,7 @@
         case 3: e1 = e0.withLabel(value.toString()); break;
         }
 
-        auto command = new ChangeEventsCommand<Model>(getId(), tr("Edit Data"));
+        auto command = new ChangeEventsCommand(getId().untyped, tr("Edit Data"));
         command->remove(e0);
         command->add(e1);
         return command->finish();
--- a/data/model/Labeller.h	Thu Jun 27 13:08:10 2019 +0100
+++ b/data/model/Labeller.h	Fri Jun 28 17:36:30 2019 +0100
@@ -225,17 +225,16 @@
      * Relabel all events in the given event vector that lie within
      * the given multi-selection, according to the labelling
      * properties of this labeller.  Return a command that has been
-     * executed but not yet added to the history. The template
-     * parameter must be a type that can be dynamic_cast to
-     * EventEditable and that has a ById store.
+     * executed but not yet added to the history.  The id must be that
+     * of a type that can be retrieved from the AnyById store and
+     * dynamic_cast to EventEditable.
      */
-    template <typename EditableBase>
-    Command *labelAll(typename EditableBase::Id editable,
+    Command *labelAll(int editableId,
                       MultiSelection *ms,
                       const EventVector &allEvents) {
 
-        auto command = new ChangeEventsCommand<EditableBase>
-            (editable, tr("Label Points"));
+        auto command = new ChangeEventsCommand
+            (editableId, tr("Label Points"));
 
         Event prev;
         bool havePrev = false;
@@ -273,18 +272,17 @@
      * that event lies within the given multi-selection, add n-1 new
      * events at equally spaced intervals between it and the following
      * event.  Return a command that has been executed but not yet
-     * added to the history. The template parameter must be a type
-     * that can be dynamic_cast to EventEditable and that has a ById
-     * store.
+     * added to the history.  The id must be that of a type that can
+     * be retrieved from the AnyById store and dynamic_cast to
+     * EventEditable.
      */
-    template <typename EditableBase>
-    Command *subdivide(typename EditableBase::Id editable,
+    Command *subdivide(int editableId,
                        MultiSelection *ms,
                        const EventVector &allEvents,
                        int n) {
 
-        auto command = new ChangeEventsCommand<EditableBase>
-            (editable, tr("Subdivide Points"));
+        auto command = new ChangeEventsCommand
+            (editableId, tr("Subdivide Points"));
 
         for (auto i = allEvents.begin(); i != allEvents.end(); ++i) {
 
@@ -325,18 +323,17 @@
      * multi-selection, and a number n, remove all but every nth event
      * from the vector within the extents of the multi-selection.
      * Return a command that has been executed but not yet added to
-     * the history. The template parameter must be a type
-     * that can be dynamic_cast to EventEditable and that has a ById
-     * store.
+     * the history. The id must be that of a type that can be
+     * retrieved from the AnyById store and dynamic_cast to
+     * EventEditable.
      */
-    template <typename EditableBase>
-    Command *winnow(typename EditableBase::Id editable,
+    Command *winnow(int editableId,
                     MultiSelection *ms,
                     const EventVector &allEvents,
                     int n) {
 
-        auto command = new ChangeEventsCommand<EditableBase>
-            (editable, tr("Winnow Points"));
+        auto command = new ChangeEventsCommand
+            (editableId, tr("Winnow Points"));
 
         int counter = 0;
         
--- a/data/model/Model.h	Thu Jun 27 13:08:10 2019 +0100
+++ b/data/model/Model.h	Fri Jun 28 17:36:30 2019 +0100
@@ -33,7 +33,7 @@
  * of data on a time scale based on an audio frame rate.
  */
 class Model : public QObject,
-              public WithId<Model>,
+              public WithTypedId<Model>,
               public XmlExportable,
               public Playable
 {
@@ -363,6 +363,6 @@
 };
 
 typedef Model::Id ModelId;
-typedef StaticById<Model, Model::Id> ModelById;
+typedef TypedById<Model, Model::Id> ModelById;
 
 #endif
--- a/data/model/NoteModel.h	Thu Jun 27 13:08:10 2019 +0100
+++ b/data/model/NoteModel.h	Fri Jun 28 17:36:30 2019 +0100
@@ -66,7 +66,7 @@
             m_valueMinimum = 33.f;
             m_valueMaximum = 88.f;
         }
-        PlayParameterRepository::getInstance()->addPlayable(this);
+        PlayParameterRepository::getInstance()->addPlayable(getId().untyped);
     }
 
     NoteModel(sv_samplerate_t sampleRate, int resolution,
@@ -87,11 +87,11 @@
                    DeferredNotifier::NOTIFY_ALWAYS :
                    DeferredNotifier::NOTIFY_DEFERRED),
         m_completion(100) {
-        PlayParameterRepository::getInstance()->addPlayable(this);
+        PlayParameterRepository::getInstance()->addPlayable(getId().untyped);
     }
 
     virtual ~NoteModel() {
-        PlayParameterRepository::getInstance()->removePlayable(this);
+        PlayParameterRepository::getInstance()->removePlayable(getId().untyped);
     }
 
     QString getTypeName() const override { return tr("Note"); }
@@ -308,7 +308,7 @@
         case 5: e1 = e0.withLabel(value.toString()); break;
         }
 
-        auto command = new ChangeEventsCommand<Model>(getId(), tr("Edit Data"));
+        auto command = new ChangeEventsCommand(getId().untyped, tr("Edit Data"));
         command->remove(e0);
         command->add(e1);
         return command->finish();
--- a/data/model/RegionModel.h	Thu Jun 27 13:08:10 2019 +0100
+++ b/data/model/RegionModel.h	Fri Jun 28 17:36:30 2019 +0100
@@ -291,7 +291,7 @@
         case 4: e1 = e0.withLabel(value.toString()); break;
         }
 
-        auto command = new ChangeEventsCommand<Model>(getId(), tr("Edit Data"));
+        auto command = new ChangeEventsCommand(getId().untyped, tr("Edit Data"));
         command->remove(e0);
         command->add(e1);
         return command->finish();
--- a/data/model/SparseOneDimensionalModel.h	Thu Jun 27 13:08:10 2019 +0100
+++ b/data/model/SparseOneDimensionalModel.h	Fri Jun 28 17:36:30 2019 +0100
@@ -53,11 +53,11 @@
                    DeferredNotifier::NOTIFY_ALWAYS :
                    DeferredNotifier::NOTIFY_DEFERRED),
         m_completion(100) {
-        PlayParameterRepository::getInstance()->addPlayable(this);
+        PlayParameterRepository::getInstance()->addPlayable(getId().untyped);
     }
 
     virtual ~SparseOneDimensionalModel() {
-        PlayParameterRepository::getInstance()->removePlayable(this);
+        PlayParameterRepository::getInstance()->removePlayable(getId().untyped);
     }
 
     QString getTypeName() const override { return tr("Sparse 1-D"); }
@@ -239,7 +239,7 @@
         case 2: e1 = e0.withLabel(value.toString()); break;
         }
 
-        auto command = new ChangeEventsCommand<Model>(getId(), tr("Edit Data"));
+        auto command = new ChangeEventsCommand(getId().untyped, tr("Edit Data"));
         command->remove(e0);
         command->add(e1);
         return command->finish();
--- a/data/model/SparseTimeValueModel.h	Thu Jun 27 13:08:10 2019 +0100
+++ b/data/model/SparseTimeValueModel.h	Fri Jun 28 17:36:30 2019 +0100
@@ -54,7 +54,7 @@
         m_completion(100) {
         // Model is playable, but may not sound (if units not Hz or
         // range unsuitable)
-        PlayParameterRepository::getInstance()->addPlayable(this);
+        PlayParameterRepository::getInstance()->addPlayable(getId().untyped);
     }
 
     SparseTimeValueModel(sv_samplerate_t sampleRate, int resolution,
@@ -73,11 +73,11 @@
         m_completion(100) {
         // Model is playable, but may not sound (if units not Hz or
         // range unsuitable)
-        PlayParameterRepository::getInstance()->addPlayable(this);
+        PlayParameterRepository::getInstance()->addPlayable(getId().untyped);
     }
 
     virtual ~SparseTimeValueModel() {
-        PlayParameterRepository::getInstance()->removePlayable(this);
+        PlayParameterRepository::getInstance()->removePlayable(getId().untyped);
     }
 
     QString getTypeName() const override { return tr("Sparse Time-Value"); }
@@ -289,7 +289,7 @@
         case 3: e1 = e0.withLabel(value.toString()); break;
         }
 
-        auto command = new ChangeEventsCommand<Model>(getId(), tr("Edit Data"));
+        auto command = new ChangeEventsCommand(getId().untyped, tr("Edit Data"));
         command->remove(e0);
         command->add(e1);
         return command->finish();
--- a/data/model/TextModel.h	Thu Jun 27 13:08:10 2019 +0100
+++ b/data/model/TextModel.h	Fri Jun 28 17:36:30 2019 +0100
@@ -226,7 +226,7 @@
         case 3: e1 = e0.withLabel(value.toString()); break;
         }
 
-        auto command = new ChangeEventsCommand<Model>(getId(), tr("Edit Data"));
+        auto command = new ChangeEventsCommand(getId().untyped, tr("Edit Data"));
         command->remove(e0);
         command->add(e1);
         return command->finish();
--- a/data/model/WritableWaveFileModel.cpp	Thu Jun 27 13:08:10 2019 +0100
+++ b/data/model/WritableWaveFileModel.cpp	Fri Jun 28 17:36:30 2019 +0100
@@ -97,7 +97,7 @@
             // model ID should be ok
             QDir dir(TempDirectory::getInstance()->getPath());
             path = dir.filePath(QString("written_%1.wav")
-                                .arg(getId().toString()));
+                                .arg(getId().untyped));
         } catch (const DirectoryCreationFailed &f) {
             SVCERR << "WritableWaveFileModel: Failed to create temporary directory" << endl;
             return;
@@ -128,7 +128,7 @@
         // the filename only needs to be unique within that
         QDir dir(TempDirectory::getInstance()->getPath());
         m_temporaryPath = dir.filePath(QString("prenorm_%1.wav")
-                                       .arg(getId().toString()));
+                                       .arg(getId().untyped));
 
         m_temporaryWriter = new WavFileWriter
             (m_temporaryPath, m_sampleRate, m_channels,
--- a/files.pri	Thu Jun 27 13:08:10 2019 +0100
+++ b/files.pri	Fri Jun 28 17:36:30 2019 +0100
@@ -150,6 +150,7 @@
 	   
 SVCORE_SOURCES = \
            base/AudioLevel.cpp \
+           base/ById.cpp \
            base/Clipboard.cpp \
            base/ColumnOp.cpp \
            base/Command.cpp \