I have reduced a real-world codebase that was confusing me to a simpler example where four variations behave differently. I’ve found a fix for my real-world problem, but I would like to understand what happens here.
Among other things I am especially curious about why adding the virtual
keyword to a function can break compilation.
The MCVE consists of four files, which I am adding a bit further below:
Uncopyable.hpp
Defines a class that has a publicunique_ptr
member, which implicitly deletes the copy constructor ofUncopyable
.main.cpp
Is mainly there to run the whole thing.preproc.hpp
Defines four preprocessor macros to debug certain behaviors.Bamboozle.hpp
Defines a class with a public functionfoo(MaybeCopyable)
and an overload thereoffoo(std::shared_ptr<MaybeCopyable>)
. In the real-world codebase the overload exists to deal with classesMaybeCopyable
that are not in fact copyable.
Depending on which of my macros I set to 1
, the program may or may not compile. I’m adding a table of how it behaves when. The behavior is the same in windows with msvc/cl and in wsl2 with g++.
Questions
templates | virtual | virt dest | expl move | |
---|---|---|---|---|
1 | 0 | 1 | 0 | success |
1 | 1 | 1 | 0 | fail |
- How does
USE_VIRTUAL
have any effect here?
The function still has the same parameter type as before and is still specified in the same class. There is no inheritance involved! But making itvirtual
breaks compilation.
templates | virtual | virt dest | expl move | |
---|---|---|---|---|
0 | 0 | 1 | 0 | fail |
1 | 0 | 1 | 0 | success |
-
When
EXPLICIT_MOVES
is0
andVIRTUAL_DESTRUCTOR
is1
, why does it compile with#define USE_TEMPLATES 1
but not without? -
Setting
EXPLICIT_MOVES
to1
fixes compilation in all cases, independent of the other three macros. What is the explanation for this?
Files
// Uncopyable.hpp
#pragma once
#include "preproc.hpp"
#include<memory>
struct Dummy {int i = 3;};
class Uncopyable {
public:
// Implicitly delete the copy constructor by having
// a non-copyable member.
std::unique_ptr<Dummy> m_innerPtr;
#if VIRTUAL_DESTRUCTOR
// A virtual destructor to allow correct inheritance.
// I first thought that this implicitly deletes the
// copy-assignment and copy-constructor,
// but it does not. It does, however, implicitly
// delete the move constructor and assignment.
virtual ~Uncopyable() = default;
#endif
#if EXPLICIT_MOVES
Uncopyable(Uncopyable&& other) noexcept = default;
Uncopyable& operator=(Uncopyable&& other) noexcept = default;
#endif
// accessible constructor
Uncopyable() = default;
Uncopyable(std::unique_ptr<Dummy> dummy):m_innerPtr(std::move(dummy)){};
};
// Bamboozle.hpp
#pragma once
#include "preproc.hpp"
#include<memory>
#include "Uncopyable.hpp"
#if USE_TEMPLATES
template <typename MaybeCopyable>
#else
using MaybeCopyable = Uncopyable;
#endif
class Bamboozle {
public:
void foo(std::shared_ptr<MaybeCopyable> obj) {
std::cout << "Reached Bamboozle::foo(shared_ptr)" << std::endl;
}
// overload that takes an object by value
#if USE_VIRTUAL
virtual
#endif
void foo(MaybeCopyable obj){
std::cout << "Reached Bamboozle::foo(Uncopyable)" << std::endl;
foo(std::make_shared<Uncopyable>(std::move(obj)));
}
};
// main.cpp
// compile with: cl /W4 /EHsc main.cpp /link /out:main.exe
#include "preproc.hpp"
#include<iostream>
#include<memory>
#include"Bamboozle.hpp"
int main(int argc, char** argv){
std::cout << "Starting..." << std::endl;
// construct param objects
Uncopyable uncObj;
std::shared_ptr<Uncopyable> uncSharedPtr = std::make_shared<Uncopyable>();
std::cout << "n-- Stage Alpha --" << std::endl;
// prints Bamboozle::foo(shared_ptr)
#if USE_TEMPLATES
Bamboozle<Uncopyable> bamboozleObj;
#else
Bamboozle bamboozleObj;
#endif
bamboozleObj.foo(uncSharedPtr);
/*
std::cout << "n-- Stage Beta --" << std::endl;
// Compiler error in all cases,
// because it is not copyable.
// Understandable.
bamboozleObj.foo(uncObj);
*/
std::cout << "nEnding..." << std::endl;
return 0;
};
// preproc.hpp
#pragma once
// USE_TEMPLATES 0 -> Fail build
// No matter the value of USE_VIRTUAL
// USE_TEMPLATES 1 -> Success
#define USE_TEMPLATES 0
// USE_VIRTUAL 0 -> Success
// USE_VIRTUAL 1 -> Fail build
// No matter the value of USE_TEMPLATES
#define USE_VIRTUAL 1
#define VIRTUAL_DESTRUCTOR 0
// This was in an earlier version, when I had a base class BaseUncopyable
// with a virtual destructor:
/*
| templates | virtual | |
| --- | --- | --- |
| 0 | 0 | fail |
| 1 | 0 | success |
| 0 | 1 | fail |
| 1 | 1 | fail |
*/
// This is when I have no such base class but might make
// the destructor of Uncopyable virtual:
// (compiled with windows: cl /W4 /EHsc main.cpp /link /out:main.exe )
/*
| templates | virtual | virt dest | |
| --- | --- | --- | --- |
| 0 | 0 | 0 | success |
| 1 | 0 | 0 | success |
| 0 | 1 | 0 | success |
| 1 | 1 | 0 | success |
| 0 | 0 | 1 | fail |
| 1 | 0 | 1 | success |
| 0 | 1 | 1 | fail |
| 1 | 1 | 1 | fail |
*/
// (compiled with wsl: g++ -Wall -Wextra -O0 -fsanitize="undefined" -o main_gcc main.cpp )
// Same behavior.
#define EXPLICIT_MOVES 1
Table
templates | virtual | virt dest | expl move | |
---|---|---|---|---|
0 | 0 | 0 | 0 | success |
1 | 0 | 0 | 0 | success |
0 | 1 | 0 | 0 | success |
1 | 1 | 0 | 0 | success |
0 | 0 | 1 | 0 | fail |
1 | 0 | 1 | 0 | success |
0 | 1 | 1 | 0 | fail |
1 | 1 | 1 | 0 | fail |
templates | virtual | virt dest | expl move | |
---|---|---|---|---|
0 | 0 | 0 | 1 | success |
1 | 0 | 0 | 1 | success |
0 | 1 | 0 | 1 | success |
1 | 1 | 0 | 1 | success |
0 | 0 | 1 | 1 | success |
1 | 0 | 1 | 1 | success |
0 | 1 | 1 | 1 | success |
1 | 1 | 1 | 1 | success |
To my understanding, VIRTUAL_DESTRUCTOR
implicitly deletes the move constructor and move assignment. With EXPLICIT_MOVES
enabled, I am defining them again. And I have nobody inheriting from Uncopyable
, so the virtual destructor itself does not matter in theory. So the behavior should be the same with both enabled as with both disabled. So we can simplify this table almost down to the statement “When the move constructor/assignment is defined, it compiles”. The bold success is the only one that is not explained by that.