5

This is a very strange issue. I have two classes: a custom console class (CConsole) and a test class (CHashTableTest) which I've made to play around with maps and unordered_maps to learn how they work.

In my console class I have a public static member variable of CConsole that exposes a static console object to the rest of the project so that I can write to this console when ever I want. This works fine for all of my classes including the test class but only when that test classes uses map and not unordered_map!

The error that I receive is:

error LNK2001: unresolved external symbol "public static class CConsole CConsole:output" (?output@CConsole@@2V1@A)

It comes from the class that calls the methods on the test class and not the test class itself but nothing strange is happening in the calling class, it simply instantiates the CHashTableTest object (passing in a CConsole object) and calls Add and Get on it. It is placed in a separate project but this isn't a problem when I use map as the static member variable has been made external using _declspec(ddlexport).

The solution is setup in Visual Studio 2012, the CConsole and CHashTableTest classes are in a DLL project which is an external reference of a Unit Test project where the calling code exists.

The CConsole and CHashTableTest files:

Console.h

#ifndef _CONSOLE_H_
#define _CONSOLE_H_

#define _CRT_SECURE_NO_DEPRECATE
#include <iostream>
#include <fstream>
#include <string>
#include <ctime>
#include <time.h>

// Defines a set of output modes for the CConsole class. This will affect what output the console will target.
// The default is COUT.
enum _declspec(dllexport) EConsoleMode 
{ 
   // Output sent to a CConsole object will be printed using cout.
   COUT,
   // Output sent to a CConsole object will be printed to a file.
   OUTPUT_FILE,
   // Output sent to a CConsole object will be printed using OutputDebugString.
   OUTPUT_DEBUG_STRING, 
   // Output sent to a CConsole object will be printed using Debug::WriteLine.
   DEBUG_WRITE_LINE, 
   // Output sent to a CConsole object will be printed using Console::WriteLine.
   CONSOLE_WRITE_LINE,
   // Output sent to a CConsole object will not be printed.
   NO_OUTPUT,
};

// An output wrapper class that allows logging and redirecting of log and debugging messages to different
// output targets. 
class _declspec(dllexport) CConsole
{
public:
   static CConsole output;

   // Constructs a CConsole object with a specific console mode, default is COUT.
   CConsole(EConsoleMode mode = COUT, const char* filePath = "C:/output.txt");
   ~CConsole(void);

   // Gets the mode of this CConsole object.
   EConsoleMode GetMode();

   const char* GetFilePath();

   // Sets the output file path of this CConsole object.
   void SetFilePath(const char* filePath);

   void TimeStamp();

   // Logs a message with this specific CConsole object. An indirect call to an operator overload of
   // CConsole << Type
   template <typename T> void Log(const T &message);

protected:
   // The mode of this CConsole object.
   EConsoleMode m_mode;

   // The file path of the output file for this CConsole object.
   const char* m_filePath;
};


// Operator overload of CConsole << Type, queries the mode of the given CConsole object and 
// selects the appropriate output method associated with the mode.
template <typename T>
CConsole operator<< (CConsole console, const T &input) 
{
   switch(console.GetMode())
   {
   case COUT:
      {
         std::cout << input << "\n";
         break;
      }
   case OUTPUT_FILE:
      {
         ofstream fout;
         fout.open (console.GetFilePath(), ios::app);
         fout << input;
         fout.close();
         break;
      }
#if ON_WINDOWS
   case OUTPUT_DEBUG_STRING:
      {
         OutputDebugString(input);
         break;
      }
   case DEBUG_WRITE_LINE:
      {
         Debug::WriteLine(input);
         break;
      }
   case CONSOLE_WRITE_LINE:
      {
         Console::WriteLine(input);
         break;
      }
#endif
   case NO_OUTPUT:
      {
         break;
      }
   default:
      {
         std::cout << input;
         break;
      }
   }
   return console;
}


// Logs a message by calling the operator overload of << on this CConsole object with the message
// parameter.
template <typename T> 
void CConsole::Log(const T &message)
{
   this << message;
}

#endif

Console.cpp

#include "Console.h"

CConsole CConsole::output = *new CConsole(OUTPUT_FILE, "C:/LocalProjects/---/output.txt"); // Known memory leak here, discussed in comments

// Constructs a CConsole object by assigning the mode parameter to the 
// m_mode member variable.
CConsole::CConsole(EConsoleMode mode, const char* filePath)
{
   m_mode = mode;
   m_filePath = filePath;
   TimeStamp();
}


CConsole::~CConsole(void)
{
   //Log("\n\n");
}


// Returns the current mode of this CConsole object.
EConsoleMode CConsole::GetMode()
{
   return m_mode;
}


const char* CConsole::GetFilePath()
{
   return m_filePath;
}

void CConsole::TimeStamp()
{
   if(m_mode == OUTPUT_FILE)
   {
      std::ofstream file;
      file.open (m_filePath, std::ios::app); // 
      std::time_t currentTime = time(nullptr);
      file << "Console started: " << std::asctime(std::localtime(&currentTime));
      file.close();
   }
}

void CConsole::SetFilePath(const char* filePath)
{
   m_filePath = filePath;
}

HashTableTest.h

#ifndef _HASH_TABLE_TEST_H_
#define _HASH_TABLE_TEST_H_

#include <unordered_map>
#include <map>
#include <vector>
#include "Debuggable.h"
#include "Console.h"

using namespace std;

//template class __declspec(dllexport) unordered_map<int, double>;

struct Hasher
{
public:
  size_t operator() (vector<int> const& key) const
  {
      return key[0];
  }
};
struct EqualFn
{
public:
  bool operator() (vector<int> const& t1, vector<int> const& t2) const
  {
     CConsole::output << "\t\tAdding, t1: " << t1[0] << ", t2: " << t2[0] << "\n"; // If I delete this line then no error!
     return (t1[0] == t2[0]);
  }
};

class __declspec(dllexport) CHashTableTest : public CDebuggable 
{
public:
   CHashTableTest(CConsole console = *new CConsole());
   ~CHashTableTest(void);
   void Add(vector<int> key, bool value);
   bool Get(vector<int> key);

private:
   CConsole m_console;
   unordered_map<vector<int>, bool, Hasher, EqualFn> m_table; // If I change this to a map then no error!
};

#endif

Just to be clear, if I change 'unordered_map' above to 'map' and remove the Hasher function object from the template argument list this will compile. If I don't I get the linker error. I haven't included HashTableTest.cpp because it contains practically nothing.

EDIT

I'm not sure if I'm using unordered_map properly here is the implementation for the CHashTableTest class:

HashTableTest.cpp

#include "HashTableTest.h"


CHashTableTest::CHashTableTest(CConsole console) : CDebuggable(console)
{
}


CHashTableTest::~CHashTableTest(void)
{
}

void CHashTableTest::Add(vector<int> key, bool value)
{
   m_table[key] = value;
}

bool CHashTableTest::Get(vector<int> key)
{
   return m_table[key];
}
sydan
  • 302
  • 3
  • 17
  • Please post the exact declaration of m_table that you use for `map`. Also, why does your `EqualFn` not represent an equality? – Sebastian Redl Apr 27 '15 at 07:56
  • Hi Sebastian, I'm not sure whether I'm using unordered_map and map properly but for map I changed the declaration to: map, bool, EqualFn> m_table; I read that the equality function should actually be treated like a 'less than' function but that might be wrong too. I'm struggling with the documentation on these. – sydan Apr 27 '15 at 08:02
  • 3
    Unrelated, rethink the sanity of whoever told you `... = *new ...` was a good idea. Its an instant-memory-leak. – WhozCraig Apr 27 '15 at 08:03
  • 1
    If at least `output` was a reference, then there would be some justification for `*new`, since the memory stays reserved until the end of the process anyway. But since it isn't, this line allocates on the heap, copy-constructs the result to the global, and then leaks the heap object. – Sebastian Redl Apr 27 '15 at 08:05
  • @WhozCraig and Sebastian, thank you for catching that, I'm new to C++, these are the sorts of things I'm not yet aware of. I'll be sure not to do that in the future. I've looked in to map and unordered_map further, seeing now that unordered_map needs equality while map needs lessthan, so I'll update that. – sydan Apr 27 '15 at 08:09
  • @sydan no worries. while doing so, review how a [strict weak ordering](http://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings) works. The ordering algorithms (such as that used by `std::map`) require it, and sometimes it isn't as intuitive as you may think. Just something to remember. – WhozCraig Apr 27 '15 at 08:12
  • Also unrelated: `using namespace std;` [is bad](http://stackoverflow.com/q/1452721/3959454), especially in .h file. It may arguably be acceptable in test code, which HashTableTest.h apparently is, but don't do this in production code. – Anton Savin Apr 27 '15 at 08:13
  • @WhozCraig that makes sense, though I'm not sure how to apply it any way other than a simple < for numerical values? Does this become more complex with less obvious data structures? – sydan Apr 27 '15 at 08:19
  • @AntonSavin my C++ practice is bad... I would have used this in production code as well. I see how this could cause serious headaches in the future, thank you for helping me. – sydan Apr 27 '15 at 08:21
  • @sydan It certainly can (otherwise I wouldn't have mentioned it). In your case you're keying on a `std::vector<>` relying on the value of its first element as the key for a `std::map<>`. Understand that given two values, if `!(a < b) && !(b < a)` then `a == b` as far as a SWO is concerned. That may seem intuitive, but then think about a *pair* of values that you want to order on the first value and only if they're *equal* sort on the second. That would play out as `(a.first < b.first || (!(b.first < a.first) && a.second < b.second))`. As I said, just something to keep in mind. – WhozCraig Apr 27 '15 at 08:24
  • regardless, still wondering about your linker error. Are you building this in a DLL project, then linking its import lib in to some test harness EXE that includes your headers? The declspecs would seem to indicate as-such. Is that the case? – WhozCraig Apr 27 '15 at 08:25
  • @WhozCraig effectively yes. I'm using Visual Studio 2012 I have a DLL project and a Unit Test project which references the DLL, as you pointed out this is the reason for the declspecs. I'll add these notes to the question. – sydan Apr 27 '15 at 08:29
  • And the unit test EXE links to the import lib from the DLL (may seem obvious, but it matters; its not doing something insane like trying to dynaload this)? If so, then your declspecs aren't quite correct. – WhozCraig Apr 27 '15 at 08:30
  • The unit test project uses a DLL rather than an EXE but essentially yes, I've set up the external dependencies to the correct DLL and lib path as far as I know. I'm not dynamically loading, stack overflow would like to move this to chat so I'm going to follow through with that. – sydan Apr 27 '15 at 08:37
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/76337/discussion-between-sydan-and-whozcraig). – sydan Apr 27 '15 at 08:37

1 Answers1

2

When a DLL's generated import library is being linked with an consuming EXE (or another DLL) the declspecs of the items being brought in should be declspec(dllimport) on the receiver-side. When you're building the actual DLL those same items are made available by usage of declspec(dllexport). Your code has the latter, but not the former.

This is normally achieved by a single preprocessor macro that is defined only for your DLL project in the preprocessor section of the compiler configuration. For example in your header(s) that are declaring things exported from your DLL:

#ifndef MYDLL_HEADER_H 

#ifdef MYDLL_EXPORTS 
#define EXPORT __declspec(dllexport) 
#else 
#define EXPORT __declspec(dllimport) 
#endif 

class EXPORT CConsole 
{ 
// stuff here. 
}; 

#endif 

Doing this will tell the compiler when building the DLL to export the class members, including class statics, because MYDLL_EXPORTS is defined in the preprocessor configuration flags for your DLL project.

When building a consuming project, do NOT define MYDLL_EXPORTSin the preprocessor configuration of the compiler settings. It will thusly use dllimport, rather than dllexport. That should square away the linker to know where to look for the things it needs.

Best of luck.

WhozCraig
  • 65,258
  • 11
  • 75
  • 141